@ -5,9 +5,11 @@ import android.app.Activity
import android.app.AlarmManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationManagerCompat
@ -20,6 +22,7 @@ import com.getcapacitor.annotation.CapacitorPlugin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONArray
import org.json.JSONObject
/ * *
@ -76,6 +79,8 @@ open class DailyNotificationPlugin : Plugin() {
private var db : DailyNotificationDatabase ? = null
private val PERMISSION_REQUEST_CODE = 1001
override fun load ( ) {
super . load ( )
try {
@ -91,6 +96,38 @@ open class DailyNotificationPlugin : Plugin() {
}
}
/ * *
* Handle app resume - check for pending permission calls
* This is a fallback since Capacitor ' s Bridge intercepts permission results
* and may not route them to our plugin ' s handleRequestPermissionsResult
* /
override fun handleOnResume ( ) {
super . handleOnResume ( )
// Check if we have a pending permission call
val call = savedCall
if ( call != null && context != null && Build . VERSION . SDK_INT >= Build . VERSION_CODES . TIRAMISU ) {
// Check if this is a permission request call by checking the method name
val methodName = call . methodName
if ( methodName == " requestPermissions " || methodName == " requestNotificationPermissions " ) {
// Check current permission status
val granted = ContextCompat . checkSelfPermission (
context !! ,
Manifest . permission . POST_NOTIFICATIONS
) == PackageManager . PERMISSION_GRANTED
val result = JSObject ( ) . apply {
put ( " status " , if ( granted ) " granted " else " denied " )
put ( " granted " , granted )
put ( " notifications " , if ( granted ) " granted " else " denied " )
}
Log . i ( TAG , " Resolving pending permission call on resume: granted= $granted " )
call . resolve ( result )
}
}
}
private fun getDatabase ( ) : DailyNotificationDatabase {
if ( db == null ) {
if ( context == null ) {
@ -174,6 +211,175 @@ open class DailyNotificationPlugin : Plugin() {
}
}
/ * *
* Check permissions ( Capacitor standard format )
* Returns PermissionStatus with notifications field as PermissionState
* /
@PluginMethod
override fun checkPermissions ( call : PluginCall ) {
try {
if ( context == null ) {
return call . reject ( " Context not available " )
}
Log . i ( TAG , " Checking permissions (Capacitor format) " )
var notificationsState = " denied "
var notificationsEnabled = false
// Check POST_NOTIFICATIONS permission
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . TIRAMISU ) {
val granted = context . checkSelfPermission ( Manifest . permission . POST_NOTIFICATIONS ) == PackageManager . PERMISSION_GRANTED
notificationsState = if ( granted ) " granted " else " prompt "
notificationsEnabled = granted
} else {
// Pre-Android 13: check if notifications are enabled
val enabled = NotificationManagerCompat . from ( context ) . areNotificationsEnabled ( )
notificationsState = if ( enabled ) " granted " else " denied "
notificationsEnabled = enabled
}
val result = JSObject ( ) . apply {
put ( " status " , notificationsState )
put ( " granted " , notificationsEnabled )
put ( " notifications " , notificationsState )
put ( " notificationsEnabled " , notificationsEnabled )
}
Log . i ( TAG , " Permissions check: notifications= $notificationsState , enabled= $notificationsEnabled " )
call . resolve ( result )
} catch ( e : Exception ) {
Log . e ( TAG , " Failed to check permissions " , e )
call . reject ( " Permission check failed: ${e.message} " )
}
}
/ * *
* Get exact alarm status
* Returns detailed information about exact alarm scheduling capability
* /
@PluginMethod
fun getExactAlarmStatus ( call : PluginCall ) {
try {
if ( context == null ) {
return call . reject ( " Context not available " )
}
Log . i ( TAG , " Getting exact alarm status " )
val supported = Build . VERSION . SDK_INT >= Build . VERSION_CODES . S
var enabled = false
var canSchedule = false
val fallbackWindow = " 15 minutes " // Standard fallback window for inexact alarms
if ( supported ) {
val alarmManager = context . getSystemService ( Context . ALARM_SERVICE ) as ? AlarmManager
enabled = alarmManager ?. canScheduleExactAlarms ( ) ?: false
canSchedule = enabled
} else {
// Pre-Android 12: exact alarms are always allowed
enabled = true
canSchedule = true
}
val result = JSObject ( ) . apply {
put ( " supported " , supported )
put ( " enabled " , enabled )
put ( " canSchedule " , canSchedule )
put ( " fallbackWindow " , fallbackWindow )
}
Log . i ( TAG , " Exact alarm status: supported= $supported , enabled= $enabled , canSchedule= $canSchedule " )
call . resolve ( result )
} catch ( e : Exception ) {
Log . e ( TAG , " Failed to get exact alarm status " , e )
call . reject ( " Exact alarm status check failed: ${e.message} " )
}
}
/ * *
* Update starred plan IDs
* Stores plan IDs in SharedPreferences for native fetcher to use
* /
@PluginMethod
fun updateStarredPlans ( call : PluginCall ) {
try {
if ( context == null ) {
return call . reject ( " Context not available " )
}
val options = call . data ?: return call . reject ( " Options are required " )
// Extract planIds array from options
// Capacitor passes arrays as JSONArray in JSObject
val planIdsValue = options . get ( " planIds " )
val planIds = mutableListOf < String > ( )
when ( planIdsValue ) {
is JSONArray -> {
// Direct JSONArray
for ( i in 0 until planIdsValue . length ( ) ) {
planIds . add ( planIdsValue . getString ( i ) )
}
}
is List < * > -> {
// List from JSObject conversion
planIds . addAll ( planIdsValue . filterIsInstance < String > ( ) )
}
is String -> {
// Single string (unlikely but handle it)
planIds . add ( planIdsValue )
}
else -> {
// Try to get as JSObject and extract array
val planIdsObj = options . getJSObject ( " planIds " )
if ( planIdsObj != null ) {
val array = planIdsObj . get ( " planIds " )
if ( array is JSONArray ) {
for ( i in 0 until array . length ( ) ) {
planIds . add ( array . getString ( i ) )
}
}
} else {
return call . reject ( " planIds array is required " )
}
}
}
Log . i ( TAG , " Updating starred plans: count= ${planIds.size} " )
// Store in SharedPreferences (matching TestNativeFetcher expectations)
val prefsName = " daily_notification_timesafari "
val keyStarredPlanIds = " starredPlanIds "
val prefs : SharedPreferences = context . getSharedPreferences ( prefsName , Context . MODE_PRIVATE )
val editor = prefs . edit ( )
// Convert planIds list to JSON array string
val jsonArray = JSONArray ( )
planIds . forEach { planId ->
jsonArray . put ( planId )
}
editor . putString ( keyStarredPlanIds , jsonArray . toString ( ) )
editor . apply ( )
val result = JSObject ( ) . apply {
put ( " success " , true )
put ( " planIdsCount " , planIds . size )
put ( " updatedAt " , System . currentTimeMillis ( ) )
}
Log . i ( TAG , " Starred plans updated: count= ${planIds.size} " )
call . resolve ( result )
} catch ( e : Exception ) {
Log . e ( TAG , " Failed to update starred plans " , e )
call . reject ( " Failed to update starred plans: ${e.message} " )
}
}
@PluginMethod
fun requestNotificationPermissions ( call : PluginCall ) {
try {
@ -194,20 +400,20 @@ open class DailyNotificationPlugin : Plugin() {
}
call . resolve ( result )
} else {
// Request permission
// Save the call using Capacitor's mechanism so it can be retrieved later
saveCall ( call )
// Request permission - result will be handled in handleRequestPermissionsResult
// Note: Capacitor's Bridge intercepts permission results, so we also check
// permission status when the app resumes as a fallback
ActivityCompat . requestPermissions (
activity ,
arrayOf ( Manifest . permission . POST_NOTIFICATIONS ) ,
1001 // Request code
PERMISSION_REQUEST_CODE
)
// Note: Permission result will be handled by onRequestPermissionsResult
// For now, resolve with pending status
val result = JSObject ( ) . apply {
put ( " status " , " prompt " )
put ( " granted " , false )
put ( " notifications " , " prompt " )
}
call . resolve ( result )
Log . i ( TAG , " Permission dialog shown, waiting for user response (requestCode= $PERMISSION_REQUEST_CODE ) " )
// Don't resolve here - wait for handleRequestPermissionsResult
}
} else {
// For older versions, permissions are granted at install time
@ -224,6 +430,51 @@ open class DailyNotificationPlugin : Plugin() {
}
}
/ * *
* Request permissions ( alias for requestNotificationPermissions )
* Delegates to requestNotificationPermissions for consistency
* /
@PluginMethod
override fun requestPermissions ( call : PluginCall ) {
requestNotificationPermissions ( call )
}
/ * *
* Handle permission request results
* Called by Capacitor when user responds to permission dialog
* /
override fun handleRequestPermissionsResult (
requestCode : Int ,
permissions : Array < out String > ,
grantResults : IntArray
) {
Log . i ( TAG , " handleRequestPermissionsResult called: requestCode= $requestCode , permissions= ${permissions.contentToString()} " )
if ( requestCode == PERMISSION_REQUEST_CODE ) {
// Retrieve the saved call
val call = savedCall
if ( call != null ) {
val granted = grantResults . isNotEmpty ( ) &&
grantResults [ 0 ] == PackageManager . PERMISSION_GRANTED
val result = JSObject ( ) . apply {
put ( " status " , if ( granted ) " granted " else " denied " )
put ( " granted " , granted )
put ( " notifications " , if ( granted ) " granted " else " denied " )
}
Log . i ( TAG , " Permission request result: granted= $granted , resolving call " )
call . resolve ( result )
return
} else {
Log . w ( TAG , " No saved call found for permission request code $requestCode " )
}
}
// Not handled by this plugin, let parent handle it
super . handleRequestPermissionsResult ( requestCode , permissions , grantResults )
}
@PluginMethod
fun configureNativeFetcher ( call : PluginCall ) {
try {