Browse Source

fix(android): implement missing plugin methods and permission handling

- Add handleOnResume() fallback to resolve permission requests when
  Capacitor Bridge doesn't route results (requestCode 1001)
- Implement checkPermissions() with override modifier for Capacitor
  standard PermissionStatus format
- Implement getExactAlarmStatus() to return exact alarm capability info
- Implement updateStarredPlans() to store plan IDs in SharedPreferences
- Fix requestPermissions() override to properly delegate to
  requestNotificationPermissions()
- Fix handleRequestPermissionsResult() return type to Unit

These changes ensure permission requests resolve correctly even when
Capacitor's Bridge doesn't recognize our custom request code, and
implement all missing methods called by the test application.
master
Matthew Raymer 1 week ago
parent
commit
1a7ac200f1
  1. 271
      android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt

271
android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt

@ -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 {

Loading…
Cancel
Save