feat(android): add exact alarm permission request flow and fix receiver mismatch
Add comprehensive exact alarm permission handling for Android 12+ (API 31+) and fix critical bugs preventing notifications from triggering. Features: - Add checkExactAlarmPermission() and requestExactAlarmPermission() plugin methods - Add canScheduleExactAlarms() and canRequestExactAlarmPermission() helper methods - Update all scheduling methods to check/request permission before scheduling - Use reflection for canRequestScheduleExactAlarms() to avoid compilation issues Bug Fixes: - Fix receiver mismatch: change alarm intents from NotifyReceiver to DailyNotificationReceiver - Fix coroutine compilation error: wrap getLatest() suspend call in runBlocking - Store notification content in database before scheduling alarms - Update intent action to match manifest registration The permission request flow opens Settings intent when SCHEDULE_EXACT_ALARM permission is not granted, providing clear user guidance. All scheduling methods now check permission status and request it if needed before proceeding. Version bumped to 1.0.8
This commit is contained in:
@@ -14,6 +14,7 @@ import androidx.core.app.NotificationCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* AlarmManager implementation for user notifications
|
||||
@@ -79,6 +80,9 @@ class NotifyReceiver : BroadcastReceiver() {
|
||||
* Uses setAlarmClock() for Android 5.0+ for better reliability
|
||||
* Falls back to setExactAndAllowWhileIdle for older versions
|
||||
*
|
||||
* FIX: Uses DailyNotificationReceiver (registered in manifest) instead of NotifyReceiver
|
||||
* Stores notification content in database and passes notification ID to receiver
|
||||
*
|
||||
* @param context Application context
|
||||
* @param triggerAtMillis When to trigger the notification (UTC milliseconds)
|
||||
* @param config Notification configuration
|
||||
@@ -93,7 +97,63 @@ class NotifyReceiver : BroadcastReceiver() {
|
||||
reminderId: String? = null
|
||||
) {
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val intent = Intent(context, NotifyReceiver::class.java).apply {
|
||||
|
||||
// Generate notification ID (use reminderId if provided, otherwise generate from trigger time)
|
||||
val notificationId = reminderId ?: "notify_${triggerAtMillis}"
|
||||
|
||||
// Store notification content in database before scheduling alarm
|
||||
// This allows DailyNotificationReceiver to retrieve content via notification ID
|
||||
// FIX: Wrap suspend function calls in coroutine
|
||||
if (!isStaticReminder) {
|
||||
try {
|
||||
// Use runBlocking to call suspend function from non-suspend context
|
||||
// This is acceptable here because we're not in a UI thread and need to ensure
|
||||
// content is stored before scheduling the alarm
|
||||
runBlocking {
|
||||
val db = DailyNotificationDatabase.getDatabase(context)
|
||||
val contentCache = db.contentCacheDao().getLatest()
|
||||
|
||||
// If we have cached content, create a notification content entity
|
||||
if (contentCache != null) {
|
||||
val roomStorage = com.timesafari.dailynotification.storage.DailyNotificationStorageRoom(context)
|
||||
val entity = com.timesafari.dailynotification.entities.NotificationContentEntity(
|
||||
notificationId,
|
||||
"1.0.2", // Plugin version
|
||||
null, // timesafariDid - can be set if available
|
||||
"daily",
|
||||
config.title,
|
||||
config.body ?: String(contentCache.payload),
|
||||
triggerAtMillis,
|
||||
java.time.ZoneId.systemDefault().id
|
||||
)
|
||||
entity.priority = when (config.priority) {
|
||||
"high", "max" -> 2
|
||||
"low", "min" -> -1
|
||||
else -> 0
|
||||
}
|
||||
entity.vibrationEnabled = config.vibration ?: true
|
||||
entity.soundEnabled = config.sound ?: true
|
||||
entity.deliveryStatus = "pending"
|
||||
entity.createdAt = System.currentTimeMillis()
|
||||
entity.updatedAt = System.currentTimeMillis()
|
||||
entity.ttlSeconds = contentCache.ttlSeconds.toLong()
|
||||
|
||||
// saveNotificationContent returns CompletableFuture, so we need to wait for it
|
||||
roomStorage.saveNotificationContent(entity).get()
|
||||
Log.d(TAG, "Stored notification content in database: id=$notificationId")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to store notification content in database, continuing with alarm scheduling", e)
|
||||
}
|
||||
}
|
||||
|
||||
// FIX: Use DailyNotificationReceiver (registered in manifest) instead of NotifyReceiver
|
||||
// FIX: Set action to match manifest registration
|
||||
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
|
||||
action = "com.timesafari.daily.NOTIFICATION" // Must match manifest intent-filter action
|
||||
putExtra("notification_id", notificationId) // DailyNotificationReceiver expects this extra
|
||||
// Also preserve original extras for backward compatibility if needed
|
||||
putExtra("title", config.title)
|
||||
putExtra("body", config.body)
|
||||
putExtra("sound", config.sound ?: true)
|
||||
@@ -188,12 +248,16 @@ class NotifyReceiver : BroadcastReceiver() {
|
||||
|
||||
/**
|
||||
* Cancel a scheduled notification alarm
|
||||
* FIX: Uses DailyNotificationReceiver to match alarm scheduling
|
||||
* @param context Application context
|
||||
* @param triggerAtMillis The trigger time of the alarm to cancel (required for unique request code)
|
||||
*/
|
||||
fun cancelNotification(context: Context, triggerAtMillis: Long) {
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val intent = Intent(context, NotifyReceiver::class.java)
|
||||
// FIX: Use DailyNotificationReceiver to match what was scheduled
|
||||
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
|
||||
action = "com.timesafari.daily.NOTIFICATION"
|
||||
}
|
||||
val requestCode = getRequestCode(triggerAtMillis)
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
@@ -207,12 +271,16 @@ class NotifyReceiver : BroadcastReceiver() {
|
||||
|
||||
/**
|
||||
* Check if an alarm is scheduled for the given trigger time
|
||||
* FIX: Uses DailyNotificationReceiver to match alarm scheduling
|
||||
* @param context Application context
|
||||
* @param triggerAtMillis The trigger time to check
|
||||
* @return true if alarm is scheduled, false otherwise
|
||||
*/
|
||||
fun isAlarmScheduled(context: Context, triggerAtMillis: Long): Boolean {
|
||||
val intent = Intent(context, NotifyReceiver::class.java)
|
||||
// FIX: Use DailyNotificationReceiver to match what was scheduled
|
||||
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
|
||||
action = "com.timesafari.daily.NOTIFICATION"
|
||||
}
|
||||
val requestCode = getRequestCode(triggerAtMillis)
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
|
||||
Reference in New Issue
Block a user