The test app UI expects 'notificationsEnabled' and 'exactAlarmEnabled'
fields from checkPermissionStatus(), but the plugin only returned
technical field names ('postNotificationsGranted', 'exactAlarmGranted').
Added compatibility fields:
- notificationsEnabled = postNotificationsGranted && notificationsEnabledAtOsLevel
- exactAlarmEnabled = exactAlarmGranted
This ensures the UI can correctly display permission status after
granting permissions.
530 lines
22 KiB
Java
530 lines
22 KiB
Java
/**
|
|
* PermissionManager.java
|
|
*
|
|
* Specialized manager for permission handling and notification settings
|
|
* Handles notification permissions, channel management, and exact alarm settings
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 2.0.0 - Modular Architecture
|
|
*/
|
|
|
|
package com.timesafari.dailynotification;
|
|
|
|
import android.Manifest;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.pm.PackageManager;
|
|
import android.os.Build;
|
|
import android.provider.Settings;
|
|
import android.util.Log;
|
|
import android.os.PowerManager;
|
|
|
|
import androidx.core.app.NotificationManagerCompat;
|
|
|
|
import com.getcapacitor.JSObject;
|
|
import com.getcapacitor.PluginCall;
|
|
|
|
/**
|
|
* Manager class for permission and settings management
|
|
*
|
|
* Responsibilities:
|
|
* - Request notification permissions
|
|
* - Check permission status
|
|
* - Manage notification channels
|
|
* - Handle exact alarm settings
|
|
* - Provide comprehensive status checking
|
|
*/
|
|
public class PermissionManager {
|
|
|
|
private static final String TAG = "PermissionManager";
|
|
|
|
private final Context context;
|
|
private final ChannelManager channelManager;
|
|
|
|
/**
|
|
* Initialize the PermissionManager
|
|
*
|
|
* @param context Android context
|
|
* @param channelManager Channel manager for notification channels
|
|
*/
|
|
public PermissionManager(Context context, ChannelManager channelManager) {
|
|
this.context = context;
|
|
this.channelManager = channelManager;
|
|
|
|
Log.d(TAG, "PermissionManager initialized");
|
|
}
|
|
|
|
/**
|
|
* Request notification permissions from the user
|
|
*
|
|
* @param call Plugin call
|
|
* @param activity Activity for showing permission dialog (required for Android 13+)
|
|
*/
|
|
public void requestNotificationPermissions(PluginCall call, android.app.Activity activity) {
|
|
try {
|
|
Log.d(TAG, "Requesting notification permissions");
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
// For Android 13+, request POST_NOTIFICATIONS permission
|
|
if (activity == null) {
|
|
call.reject("Activity not available - required for permission request");
|
|
return;
|
|
}
|
|
|
|
// Check if already granted
|
|
if (androidx.core.content.ContextCompat.checkSelfPermission(context,
|
|
android.Manifest.permission.POST_NOTIFICATIONS)
|
|
== android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
|
// Already granted
|
|
JSObject result = new JSObject();
|
|
result.put("status", "granted");
|
|
result.put("granted", true);
|
|
result.put("notifications", "granted");
|
|
call.resolve(result);
|
|
} else {
|
|
// Request permission - activity must handle result via handleRequestPermissionsResult
|
|
// Note: The plugin should save the call before calling this method
|
|
androidx.core.app.ActivityCompat.requestPermissions(
|
|
activity,
|
|
new String[]{android.Manifest.permission.POST_NOTIFICATIONS},
|
|
com.timesafari.dailynotification.DailyNotificationConstants.PERMISSION_REQUEST_CODE // Centralized constant
|
|
);
|
|
|
|
Log.d(TAG, "Permission dialog shown, waiting for user response");
|
|
// Don't resolve here - wait for handleRequestPermissionsResult in plugin
|
|
}
|
|
} else {
|
|
// For older versions, permissions are granted at install time
|
|
JSObject result = new JSObject();
|
|
result.put("status", "granted");
|
|
result.put("granted", true);
|
|
result.put("notifications", "granted");
|
|
call.resolve(result);
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error requesting notification permissions", e);
|
|
call.reject("Failed to request permissions: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request notification permissions from the user (backward compatibility - requires activity)
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
public void requestNotificationPermissions(PluginCall call) {
|
|
// This version cannot actually request permissions without activity
|
|
// It will only check if already granted
|
|
requestPermission(Manifest.permission.POST_NOTIFICATIONS, call);
|
|
}
|
|
|
|
/**
|
|
* Get comprehensive permission status
|
|
* Returns PermissionStatus model (single source of truth)
|
|
*
|
|
* @return PermissionStatus with all permission states
|
|
*/
|
|
public com.timesafari.dailynotification.PermissionStatus getPermissionStatus() {
|
|
boolean postNotificationsGranted = false;
|
|
boolean exactAlarmsGranted = false;
|
|
boolean notificationsEnabledAtOsLevel = false;
|
|
boolean batteryOptimizationsIgnored = false;
|
|
|
|
// Check POST_NOTIFICATIONS permission (Android 13+)
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
postNotificationsGranted = context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
|
|
== PackageManager.PERMISSION_GRANTED;
|
|
} else {
|
|
// Pre-Android 13: check OS-level notification enablement
|
|
postNotificationsGranted = true; // Permission granted at install time
|
|
}
|
|
|
|
// Always check OS-level notification enablement (important for all API levels)
|
|
notificationsEnabledAtOsLevel = NotificationManagerCompat.from(context).areNotificationsEnabled();
|
|
|
|
// Check exact alarm permission (Android 12+)
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
android.app.AlarmManager alarmManager = (android.app.AlarmManager)
|
|
context.getSystemService(Context.ALARM_SERVICE);
|
|
exactAlarmsGranted = alarmManager != null && alarmManager.canScheduleExactAlarms();
|
|
} else {
|
|
exactAlarmsGranted = true; // Pre-Android 12, exact alarms are always allowed
|
|
}
|
|
|
|
// Check battery optimizations (Android 6+)
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
try {
|
|
android.os.PowerManager powerManager = (android.os.PowerManager)
|
|
context.getSystemService(Context.POWER_SERVICE);
|
|
if (powerManager != null) {
|
|
batteryOptimizationsIgnored = powerManager.isIgnoringBatteryOptimizations(context.getPackageName());
|
|
}
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "Error checking battery optimizations", e);
|
|
batteryOptimizationsIgnored = false;
|
|
}
|
|
} else {
|
|
batteryOptimizationsIgnored = true; // Pre-Android 6, no battery optimization restrictions
|
|
}
|
|
|
|
return new com.timesafari.dailynotification.PermissionStatus(
|
|
postNotificationsGranted,
|
|
exactAlarmsGranted,
|
|
batteryOptimizationsIgnored,
|
|
notificationsEnabledAtOsLevel,
|
|
Build.VERSION.SDK_INT
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check the current status of notification permissions
|
|
* Delegates to getPermissionStatus() and formats response for JS
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
public void checkPermissionStatus(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Checking permission status");
|
|
|
|
com.timesafari.dailynotification.PermissionStatus status = getPermissionStatus();
|
|
|
|
JSObject result = status.toJSObject();
|
|
result.put("success", true);
|
|
result.put("channelEnabled", channelManager.isChannelEnabled());
|
|
result.put("channelImportance", channelManager.getChannelImportance());
|
|
|
|
// Add UI-friendly field names for compatibility
|
|
// notificationsEnabled = postNotificationsGranted AND notificationsEnabledAtOsLevel
|
|
boolean postNotificationsGranted = result.getBoolean("postNotificationsGranted", false);
|
|
boolean notificationsEnabledAtOsLevel = result.getBoolean("notificationsEnabledAtOsLevel", false);
|
|
result.put("notificationsEnabled", postNotificationsGranted && notificationsEnabledAtOsLevel);
|
|
// exactAlarmEnabled = exactAlarmGranted
|
|
boolean exactAlarmGranted = result.getBoolean("exactAlarmGranted", false);
|
|
result.put("exactAlarmEnabled", exactAlarmGranted);
|
|
|
|
call.resolve(result);
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error checking permission status", e);
|
|
call.reject("Failed to check permissions: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check exact alarm permission status
|
|
* Returns detailed information about permission status and whether it can be requested
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
public void checkExactAlarmPermission(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Checking exact alarm permission");
|
|
|
|
boolean canSchedule = false;
|
|
boolean canRequest = false;
|
|
boolean required = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S;
|
|
|
|
if (required) {
|
|
// Check if exact alarms can be scheduled
|
|
android.app.AlarmManager alarmManager = (android.app.AlarmManager)
|
|
context.getSystemService(Context.ALARM_SERVICE);
|
|
canSchedule = alarmManager != null && alarmManager.canScheduleExactAlarms();
|
|
|
|
// Check if permission can be requested (Android 13+)
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
// Try reflection to call Settings.canRequestScheduleExactAlarms()
|
|
try {
|
|
java.lang.reflect.Method method = Settings.class.getMethod(
|
|
"canRequestScheduleExactAlarms",
|
|
Context.class
|
|
);
|
|
canRequest = (Boolean) method.invoke(null, context);
|
|
} catch (Exception e) {
|
|
// Fallback heuristic: if exact alarms are not currently allowed,
|
|
// assume we can request them (safe default)
|
|
canRequest = !canSchedule;
|
|
}
|
|
} else {
|
|
// Android 12 (API 31-32) - permission can always be requested
|
|
canRequest = true;
|
|
}
|
|
} else {
|
|
// Android 11 and below - permission not needed
|
|
canSchedule = true;
|
|
canRequest = true;
|
|
}
|
|
|
|
JSObject result = new JSObject();
|
|
result.put("canSchedule", canSchedule);
|
|
result.put("canRequest", canRequest);
|
|
result.put("required", required);
|
|
|
|
call.resolve(result);
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error checking exact alarm permission", e);
|
|
call.reject("Permission check failed: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request exact alarm permission
|
|
* Opens Settings intent to let user grant the permission
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
public void requestExactAlarmPermission(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Requesting exact alarm permission");
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
|
// Android 11 and below don't need this permission
|
|
JSObject result = new JSObject();
|
|
result.put("success", true);
|
|
result.put("message", "Exact alarm permission not required on this Android version");
|
|
call.resolve(result);
|
|
return;
|
|
}
|
|
|
|
// Check if permission is already granted
|
|
android.app.AlarmManager alarmManager = (android.app.AlarmManager)
|
|
context.getSystemService(Context.ALARM_SERVICE);
|
|
boolean canSchedule = alarmManager != null && alarmManager.canScheduleExactAlarms();
|
|
|
|
if (canSchedule) {
|
|
// Permission already granted
|
|
JSObject result = new JSObject();
|
|
result.put("success", true);
|
|
result.put("message", "Exact alarm permission already granted");
|
|
call.resolve(result);
|
|
return;
|
|
}
|
|
|
|
// Check if app can request the permission (Android 13+)
|
|
boolean canRequest = false;
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
// Try reflection to call Settings.canRequestScheduleExactAlarms()
|
|
try {
|
|
java.lang.reflect.Method method = Settings.class.getMethod(
|
|
"canRequestScheduleExactAlarms",
|
|
Context.class
|
|
);
|
|
canRequest = (Boolean) method.invoke(null, context);
|
|
} catch (Exception e) {
|
|
// Fallback heuristic: if exact alarms are not currently allowed,
|
|
// assume we can request them (safe default)
|
|
canRequest = !canSchedule;
|
|
}
|
|
} else {
|
|
// Android 12 (API 31-32) - permission can always be requested
|
|
canRequest = true;
|
|
}
|
|
|
|
if (canRequest) {
|
|
// Open Settings to let user grant permission
|
|
try {
|
|
Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
|
|
intent.setData(android.net.Uri.parse("package:" + context.getPackageName()));
|
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
context.startActivity(intent);
|
|
|
|
JSObject result = new JSObject();
|
|
result.put("success", true);
|
|
result.put("message", "Please grant 'Alarms & reminders' permission in Settings");
|
|
call.resolve(result);
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Failed to open exact alarm settings", e);
|
|
call.reject("Failed to open exact alarm settings: " + e.getMessage());
|
|
}
|
|
} else {
|
|
// User has already denied or permission is permanently denied
|
|
// Direct user to app settings
|
|
try {
|
|
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
|
intent.setData(android.net.Uri.parse("package:" + context.getPackageName()));
|
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
context.startActivity(intent);
|
|
|
|
call.reject(
|
|
"Permission denied. Please enable 'Alarms & reminders' in app settings.",
|
|
"PERMISSION_DENIED"
|
|
);
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Failed to open app settings", e);
|
|
call.reject("Failed to open app settings: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error requesting exact alarm permission", e);
|
|
call.reject("Permission request failed: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open exact alarm settings for the user
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
public void openExactAlarmSettings(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Opening exact alarm settings");
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
|
|
intent.setData(android.net.Uri.parse("package:" + context.getPackageName()));
|
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
|
|
try {
|
|
context.startActivity(intent);
|
|
|
|
JSObject result = new JSObject();
|
|
result.put("success", true);
|
|
result.put("message", "Exact alarm settings opened");
|
|
call.resolve(result);
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Failed to open exact alarm settings", e);
|
|
call.reject("Failed to open exact alarm settings: " + e.getMessage());
|
|
}
|
|
} else {
|
|
JSObject result = new JSObject();
|
|
result.put("success", true);
|
|
result.put("message", "Exact alarms not supported on this Android version");
|
|
call.resolve(result);
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error opening exact alarm settings", e);
|
|
call.reject("Failed to open exact alarm settings: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if the notification channel is enabled
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
public void isChannelEnabled(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Checking channel status");
|
|
|
|
boolean enabled = channelManager.isChannelEnabled();
|
|
int importance = channelManager.getChannelImportance();
|
|
|
|
JSObject result = new JSObject();
|
|
result.put("success", true);
|
|
result.put("enabled", enabled);
|
|
result.put("importance", importance);
|
|
result.put("channelId", channelManager.getDefaultChannelId());
|
|
|
|
call.resolve(result);
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error checking channel status", e);
|
|
call.reject("Failed to check channel status: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open notification channel settings for the user
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
public void openChannelSettings(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Opening channel settings");
|
|
|
|
boolean opened = channelManager.openChannelSettings();
|
|
|
|
JSObject result = new JSObject();
|
|
result.put("success", true);
|
|
result.put("opened", opened);
|
|
result.put("message", opened ? "Channel settings opened" : "Failed to open channel settings");
|
|
call.resolve(result);
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error opening channel settings", e);
|
|
call.reject("Failed to open channel settings: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get comprehensive status of the notification system
|
|
*
|
|
* @param call Plugin call
|
|
*/
|
|
public void checkStatus(PluginCall call) {
|
|
try {
|
|
Log.d(TAG, "Checking comprehensive status");
|
|
|
|
// Check permissions
|
|
boolean postNotificationsGranted = false;
|
|
boolean exactAlarmsGranted = false;
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
postNotificationsGranted = context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
|
|
== PackageManager.PERMISSION_GRANTED;
|
|
} else {
|
|
postNotificationsGranted = NotificationManagerCompat.from(context).areNotificationsEnabled();
|
|
}
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
android.app.AlarmManager alarmManager = (android.app.AlarmManager)
|
|
context.getSystemService(Context.ALARM_SERVICE);
|
|
exactAlarmsGranted = alarmManager.canScheduleExactAlarms();
|
|
} else {
|
|
exactAlarmsGranted = true;
|
|
}
|
|
|
|
// Check channel status
|
|
boolean channelEnabled = channelManager.isChannelEnabled();
|
|
int channelImportance = channelManager.getChannelImportance();
|
|
|
|
// Determine overall status
|
|
boolean canScheduleNow = postNotificationsGranted && channelEnabled && exactAlarmsGranted;
|
|
|
|
JSObject result = new JSObject();
|
|
result.put("success", true);
|
|
result.put("canScheduleNow", canScheduleNow);
|
|
result.put("postNotificationsGranted", postNotificationsGranted);
|
|
result.put("exactAlarmsGranted", exactAlarmsGranted);
|
|
result.put("channelEnabled", channelEnabled);
|
|
result.put("channelImportance", channelImportance);
|
|
result.put("channelId", channelManager.getDefaultChannelId());
|
|
result.put("androidVersion", Build.VERSION.SDK_INT);
|
|
|
|
call.resolve(result);
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error checking comprehensive status", e);
|
|
call.reject("Failed to check status: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request a specific permission
|
|
*
|
|
* @param permission Permission to request
|
|
* @param call Plugin call
|
|
*/
|
|
private void requestPermission(String permission, PluginCall call) {
|
|
try {
|
|
// This would typically be handled by the Capacitor framework
|
|
// For now, we'll check if the permission is already granted
|
|
boolean granted = context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
|
|
|
|
JSObject result = new JSObject();
|
|
result.put("success", true);
|
|
result.put("granted", granted);
|
|
result.put("permission", permission);
|
|
result.put("message", granted ? "Permission already granted" : "Permission not granted");
|
|
call.resolve(result);
|
|
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Error requesting permission: " + permission, e);
|
|
call.reject("Failed to request permission: " + e.getMessage());
|
|
}
|
|
}
|
|
}
|