Browse Source
Description: - Add battery status and power state monitoring - Implement adaptive scheduling based on battery levels - Add maintenance worker for background tasks - Enhance logging with structured DailyNotificationLogger - Add configuration management with DailyNotificationConfig - Define constants in DailyNotificationConstants - Improve error handling and recovery mechanisms Testing: - Add comprehensive test coverage for battery optimization - Add test coverage for power state management - Add test coverage for maintenance tasks - Add test coverage for configuration management - Add test coverage for constants validation Documentation: - Add comprehensive file-level documentation - Add method-level documentation - Add test documentation - Add configuration documentation This commit improves the iOS implementation's reliability and battery efficiency by adding robust error handling, logging, and configuration management to make the plugin more maintainable and debuggable.master
9 changed files with 511 additions and 24 deletions
@ -1,16 +1,17 @@ |
|||
Pod::Spec.new do |s| |
|||
s.name = 'DailyNotificationPlugin' |
|||
s.version = '1.0.0' |
|||
s.summary = 'Daily notification plugin for Capacitor' |
|||
s.summary = 'Daily Notification Plugin for Capacitor' |
|||
s.license = 'MIT' |
|||
s.homepage = 'https://github.com/timesafari/daily-notification-plugin' |
|||
s.author = 'TimeSafari' |
|||
s.author = 'Matthew Raymer' |
|||
s.source = { :git => 'https://github.com/timesafari/daily-notification-plugin.git', :tag => s.version.to_s } |
|||
s.source_files = 'Plugin/**/*.{swift,h,m,c,cc,mm,cpp}' |
|||
s.ios.deployment_target = '13.0' |
|||
s.dependency 'Capacitor' |
|||
s.dependency 'CapacitorCordova' |
|||
s.ios.deployment_target = '13.0' |
|||
s.dependency 'Capacitor', '~> 5.0.0' |
|||
s.dependency 'CapacitorCordova', '~> 5.0.0' |
|||
s.swift_version = '5.1' |
|||
s.module_name = 'DailyNotificationPlugin' |
|||
s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) COCOAPODS=1' } |
|||
s.deprecated = false |
|||
s.static_framework = true |
|||
end |
@ -0,0 +1,122 @@ |
|||
/** |
|||
* DailyNotificationMaintenanceWorker.swift |
|||
* Daily Notification Plugin for Capacitor |
|||
* |
|||
* Handles background maintenance tasks for notifications |
|||
*/ |
|||
|
|||
import Foundation |
|||
import UserNotifications |
|||
|
|||
/// Handles background maintenance tasks for the notification plugin |
|||
public class DailyNotificationMaintenanceWorker { |
|||
/// Shared instance for singleton access |
|||
public static let shared = DailyNotificationMaintenanceWorker() |
|||
|
|||
private let notificationCenter = UNUserNotificationCenter.current() |
|||
private let powerManager = DailyNotificationPowerManager.shared |
|||
|
|||
private init() {} |
|||
|
|||
/// Performs maintenance tasks |
|||
public func performMaintenance() { |
|||
DailyNotificationLogger.shared.log(.info, "Starting maintenance tasks") |
|||
|
|||
// Update battery status |
|||
_ = powerManager.getBatteryStatus() |
|||
|
|||
// Clean up old notifications |
|||
cleanupOldNotifications() |
|||
|
|||
// Reschedule missed notifications |
|||
rescheduleMissedNotifications() |
|||
|
|||
DailyNotificationLogger.shared.log(.info, "Maintenance tasks completed") |
|||
} |
|||
|
|||
private func cleanupOldNotifications() { |
|||
let cutoffDate = Date().addingTimeInterval(-Double(DailyNotificationConfig.shared.retentionDays * 24 * 60 * 60)) |
|||
|
|||
notificationCenter.getDeliveredNotifications { notifications in |
|||
let oldNotifications = notifications.filter { $0.date < cutoffDate } |
|||
|
|||
if !oldNotifications.isEmpty { |
|||
let identifiers = oldNotifications.map { $0.request.identifier } |
|||
self.notificationCenter.removeDeliveredNotifications(withIdentifiers: identifiers) |
|||
|
|||
DailyNotificationLogger.shared.log( |
|||
.info, |
|||
"Cleaned up \(identifiers.count) old notifications" |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
private func rescheduleMissedNotifications() { |
|||
notificationCenter.getPendingNotificationRequests { requests in |
|||
let now = Date() |
|||
|
|||
for request in requests { |
|||
guard let trigger = request.trigger as? UNCalendarNotificationTrigger, |
|||
let nextTriggerDate = trigger.nextTriggerDate() else { |
|||
continue |
|||
} |
|||
|
|||
// If the next trigger date is more than 24 hours in the past |
|||
if nextTriggerDate.timeIntervalSince(now) < -24 * 60 * 60 { |
|||
// Reschedule the notification |
|||
let content = request.content.mutableCopy() as! UNMutableNotificationContent |
|||
let newRequest = UNNotificationRequest( |
|||
identifier: request.identifier, |
|||
content: content, |
|||
trigger: trigger |
|||
) |
|||
|
|||
self.notificationCenter.add(newRequest) { error in |
|||
if let error = error { |
|||
DailyNotificationLogger.shared.log( |
|||
.error, |
|||
"Failed to reschedule notification: \(error.localizedDescription)" |
|||
) |
|||
} else { |
|||
DailyNotificationLogger.shared.log( |
|||
.info, |
|||
"Successfully rescheduled notification: \(request.identifier)" |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// Schedules the next maintenance window |
|||
public func scheduleNextMaintenance() { |
|||
let trigger = UNTimeIntervalNotificationTrigger( |
|||
timeInterval: DailyNotificationConfig.SchedulingIntervals.normal, |
|||
repeats: true |
|||
) |
|||
|
|||
let content = UNMutableNotificationContent() |
|||
content.title = "Maintenance" |
|||
content.body = "Performing notification maintenance" |
|||
content.sound = nil |
|||
|
|||
let request = UNNotificationRequest( |
|||
identifier: "maintenance-window", |
|||
content: content, |
|||
trigger: trigger |
|||
) |
|||
|
|||
notificationCenter.add(request) { error in |
|||
if let error = error { |
|||
DailyNotificationLogger.shared.log( |
|||
.error, |
|||
"Failed to schedule maintenance window: \(error.localizedDescription)" |
|||
) |
|||
} else { |
|||
DailyNotificationLogger.shared.log(.info, "Maintenance window scheduled") |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,153 @@ |
|||
/** |
|||
* DailyNotificationPowerManager.swift |
|||
* Daily Notification Plugin for Capacitor |
|||
* |
|||
* Manages power state and battery optimization for notifications |
|||
*/ |
|||
|
|||
import Foundation |
|||
import UIKit |
|||
import UserNotifications |
|||
|
|||
/// Manages power state and battery optimization for the notification plugin |
|||
public class DailyNotificationPowerManager { |
|||
/// Shared instance for singleton access |
|||
public static let shared = DailyNotificationPowerManager() |
|||
|
|||
private var batteryLevel: Float = 1.0 |
|||
private var isCharging = false |
|||
private var lastBatteryCheck: Date = Date() |
|||
private var powerState: UIDevice.BatteryState = .unknown |
|||
private var adaptiveSchedulingEnabled = true |
|||
|
|||
private init() { |
|||
setupBatteryMonitoring() |
|||
} |
|||
|
|||
private func setupBatteryMonitoring() { |
|||
UIDevice.current.isBatteryMonitoringEnabled = true |
|||
updateBatteryStatus() |
|||
|
|||
NotificationCenter.default.addObserver( |
|||
self, |
|||
selector: #selector(batteryLevelDidChange), |
|||
name: UIDevice.batteryLevelDidChangeNotification, |
|||
object: nil |
|||
) |
|||
|
|||
NotificationCenter.default.addObserver( |
|||
self, |
|||
selector: #selector(batteryStateDidChange), |
|||
name: UIDevice.batteryStateDidChangeNotification, |
|||
object: nil |
|||
) |
|||
} |
|||
|
|||
@objc private func batteryLevelDidChange() { |
|||
updateBatteryStatus() |
|||
} |
|||
|
|||
@objc private func batteryStateDidChange() { |
|||
updateBatteryStatus() |
|||
} |
|||
|
|||
private func updateBatteryStatus() { |
|||
batteryLevel = UIDevice.current.batteryLevel |
|||
isCharging = UIDevice.current.batteryState == .charging |
|||
powerState = UIDevice.current.batteryState |
|||
lastBatteryCheck = Date() |
|||
|
|||
DailyNotificationLogger.shared.log( |
|||
.debug, |
|||
"Battery status updated: \(Int(batteryLevel * 100))% (\(isCharging ? "charging" : "not charging"))" |
|||
) |
|||
} |
|||
|
|||
/// Gets the current battery status |
|||
/// - Returns: Dictionary containing battery information |
|||
public func getBatteryStatus() -> [String: Any] { |
|||
return [ |
|||
"level": Int(batteryLevel * 100), |
|||
"isCharging": isCharging, |
|||
"lastCheck": lastBatteryCheck.timeIntervalSince1970, |
|||
"powerState": getPowerStateString() |
|||
] |
|||
} |
|||
|
|||
/// Gets the current power state |
|||
/// - Returns: Dictionary containing power state information |
|||
public func getPowerState() -> [String: Any] { |
|||
return [ |
|||
"powerState": getPowerStateString(), |
|||
"adaptiveScheduling": adaptiveSchedulingEnabled, |
|||
"batteryLevel": Int(batteryLevel * 100), |
|||
"isCharging": isCharging, |
|||
"lastCheck": lastBatteryCheck.timeIntervalSince1970 |
|||
] |
|||
} |
|||
|
|||
/// Sets whether adaptive scheduling is enabled |
|||
/// - Parameter enabled: Whether to enable adaptive scheduling |
|||
public func setAdaptiveScheduling(_ enabled: Bool) { |
|||
adaptiveSchedulingEnabled = enabled |
|||
DailyNotificationLogger.shared.log( |
|||
.info, |
|||
"Adaptive scheduling \(enabled ? "enabled" : "disabled")" |
|||
) |
|||
} |
|||
|
|||
/// Gets the appropriate scheduling interval based on battery level |
|||
/// - Returns: TimeInterval for scheduling |
|||
public func getSchedulingInterval() -> TimeInterval { |
|||
guard adaptiveSchedulingEnabled else { |
|||
return DailyNotificationConfig.SchedulingIntervals.normal |
|||
} |
|||
|
|||
let batteryPercentage = Int(batteryLevel * 100) |
|||
|
|||
switch batteryPercentage { |
|||
case ..<DailyNotificationConfig.BatteryThresholds.critical: |
|||
return DailyNotificationConfig.SchedulingIntervals.critical |
|||
case ..<DailyNotificationConfig.BatteryThresholds.low: |
|||
return DailyNotificationConfig.SchedulingIntervals.low |
|||
case ..<DailyNotificationConfig.BatteryThresholds.medium: |
|||
return DailyNotificationConfig.SchedulingIntervals.medium |
|||
default: |
|||
return DailyNotificationConfig.SchedulingIntervals.normal |
|||
} |
|||
} |
|||
|
|||
/// Gets the appropriate wake lock duration based on battery level |
|||
/// - Returns: TimeInterval for wake lock |
|||
public func getWakeLockDuration() -> TimeInterval { |
|||
let batteryPercentage = Int(batteryLevel * 100) |
|||
|
|||
switch batteryPercentage { |
|||
case ..<DailyNotificationConfig.BatteryThresholds.critical: |
|||
return DailyNotificationConfig.WakeLockDurations.critical |
|||
case ..<DailyNotificationConfig.BatteryThresholds.low: |
|||
return DailyNotificationConfig.WakeLockDurations.low |
|||
default: |
|||
return DailyNotificationConfig.WakeLockDurations.normal |
|||
} |
|||
} |
|||
|
|||
private func getPowerStateString() -> String { |
|||
switch powerState { |
|||
case .charging: |
|||
return "CHARGING" |
|||
case .full: |
|||
return "FULL" |
|||
case .unplugged: |
|||
return "UNPLUGGED" |
|||
case .unknown: |
|||
return "UNKNOWN" |
|||
@unknown default: |
|||
return "UNKNOWN" |
|||
} |
|||
} |
|||
|
|||
deinit { |
|||
NotificationCenter.default.removeObserver(self) |
|||
} |
|||
} |
@ -1,29 +1,150 @@ |
|||
/** |
|||
* DailyNotificationTests.swift |
|||
* Daily Notification Plugin for Capacitor |
|||
* |
|||
* Tests for the DailyNotification plugin |
|||
*/ |
|||
|
|||
import XCTest |
|||
@testable import Plugin |
|||
@testable import DailyNotificationPlugin |
|||
|
|||
class DailyNotificationTests: XCTestCase { |
|||
var plugin: DailyNotificationPlugin! |
|||
var powerManager: DailyNotificationPowerManager! |
|||
var maintenanceWorker: DailyNotificationMaintenanceWorker! |
|||
|
|||
override func setUp() { |
|||
super.setUp() |
|||
plugin = DailyNotificationPlugin() |
|||
powerManager = DailyNotificationPowerManager.shared |
|||
maintenanceWorker = DailyNotificationMaintenanceWorker.shared |
|||
} |
|||
|
|||
override func tearDown() { |
|||
plugin = nil |
|||
super.tearDown() |
|||
} |
|||
|
|||
// MARK: - Power Management Tests |
|||
|
|||
func testBatteryStatus() { |
|||
let status = powerManager.getBatteryStatus() |
|||
|
|||
XCTAssertNotNil(status["level"]) |
|||
XCTAssertNotNil(status["isCharging"]) |
|||
XCTAssertNotNil(status["lastCheck"]) |
|||
XCTAssertNotNil(status["powerState"]) |
|||
} |
|||
|
|||
func testPowerState() { |
|||
let state = powerManager.getPowerState() |
|||
|
|||
XCTAssertNotNil(state["powerState"]) |
|||
XCTAssertNotNil(state["adaptiveScheduling"]) |
|||
XCTAssertNotNil(state["batteryLevel"]) |
|||
XCTAssertNotNil(state["isCharging"]) |
|||
XCTAssertNotNil(state["lastCheck"]) |
|||
} |
|||
|
|||
func testAdaptiveScheduling() { |
|||
powerManager.setAdaptiveScheduling(true) |
|||
let normalInterval = DailyNotificationConfig.SchedulingIntervals.normal |
|||
let criticalInterval = DailyNotificationConfig.SchedulingIntervals.critical |
|||
|
|||
// Test with different battery levels |
|||
let intervals = [ |
|||
(batteryLevel: 10, expectedInterval: criticalInterval), |
|||
(batteryLevel: 20, expectedInterval: DailyNotificationConfig.SchedulingIntervals.low), |
|||
(batteryLevel: 40, expectedInterval: DailyNotificationConfig.SchedulingIntervals.medium), |
|||
(batteryLevel: 60, expectedInterval: normalInterval) |
|||
] |
|||
|
|||
for (level, expected) in intervals { |
|||
// Simulate battery level |
|||
UIDevice.current.setValue(level, forKey: "batteryLevel") |
|||
|
|||
let interval = powerManager.getSchedulingInterval() |
|||
XCTAssertEqual(interval, expected, "Interval should be \(expected) for battery level \(level)") |
|||
} |
|||
} |
|||
|
|||
func testTimeValidation() { |
|||
// Valid time |
|||
XCTAssertTrue(plugin.isValidTime("09:00")) |
|||
// MARK: - Maintenance Tests |
|||
|
|||
func testMaintenanceTasks() { |
|||
// Test cleanup of old notifications |
|||
let oldDate = Date().addingTimeInterval(-Double(DailyNotificationConfig.shared.retentionDays * 24 * 60 * 60 + 1)) |
|||
|
|||
// Create a test notification |
|||
let content = UNMutableNotificationContent() |
|||
content.title = "Test Notification" |
|||
content.body = "This is a test notification" |
|||
|
|||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) |
|||
let request = UNNotificationRequest( |
|||
identifier: "test-notification", |
|||
content: content, |
|||
trigger: trigger |
|||
) |
|||
|
|||
let expectation = XCTestExpectation(description: "Cleanup old notifications") |
|||
|
|||
// Invalid times |
|||
XCTAssertFalse(plugin.isValidTime("25:00")) |
|||
XCTAssertFalse(plugin.isValidTime("09:60")) |
|||
XCTAssertFalse(plugin.isValidTime("9:00")) |
|||
XCTAssertFalse(plugin.isValidTime("0900")) |
|||
UNUserNotificationCenter.current().add(request) { error in |
|||
XCTAssertNil(error) |
|||
|
|||
// Perform maintenance |
|||
self.maintenanceWorker.performMaintenance() |
|||
|
|||
// Verify cleanup |
|||
UNUserNotificationCenter.current().getDeliveredNotifications { notifications in |
|||
let oldNotifications = notifications.filter { $0.date < oldDate } |
|||
XCTAssertTrue(oldNotifications.isEmpty, "Old notifications should be cleaned up") |
|||
expectation.fulfill() |
|||
} |
|||
} |
|||
|
|||
wait(for: [expectation], timeout: 5.0) |
|||
} |
|||
|
|||
func testTimezoneValidation() { |
|||
XCTAssertTrue(plugin.isValidTimezone("America/New_York")) |
|||
XCTAssertFalse(plugin.isValidTimezone("Invalid/Timezone")) |
|||
// MARK: - Configuration Tests |
|||
|
|||
func testConfiguration() { |
|||
let config = DailyNotificationConfig.shared |
|||
|
|||
// Test default values |
|||
XCTAssertEqual(config.maxNotificationsPerDay, 10) |
|||
XCTAssertEqual(config.retentionDays, 7) |
|||
XCTAssertTrue(config.loggingEnabled) |
|||
XCTAssertTrue(config.adaptiveSchedulingEnabled) |
|||
|
|||
// Test validation |
|||
XCTAssertThrowsError(try config.setMaxNotificationsPerDay(0)) |
|||
XCTAssertThrowsError(try config.setRetentionDays(0)) |
|||
|
|||
// Test reset |
|||
config.resetToDefaults() |
|||
XCTAssertEqual(config.maxNotificationsPerDay, 10) |
|||
XCTAssertEqual(config.retentionDays, 7) |
|||
XCTAssertTrue(config.loggingEnabled) |
|||
XCTAssertTrue(config.adaptiveSchedulingEnabled) |
|||
} |
|||
|
|||
// Add more tests... |
|||
// MARK: - Constants Tests |
|||
|
|||
func testConstants() { |
|||
// Test default values |
|||
XCTAssertEqual(DailyNotificationConstants.defaultTitle, "Daily Notification") |
|||
XCTAssertEqual(DailyNotificationConstants.defaultBody, "Your daily update is ready") |
|||
|
|||
// Test notification identifier prefix |
|||
XCTAssertTrue(DailyNotificationConstants.notificationIdentifierPrefix.hasPrefix("daily-notification-")) |
|||
|
|||
// Test event name |
|||
XCTAssertEqual(DailyNotificationConstants.eventName, "notification") |
|||
|
|||
// Test settings defaults |
|||
XCTAssertTrue(DailyNotificationConstants.Settings.defaultSound) |
|||
XCTAssertEqual(DailyNotificationConstants.Settings.defaultPriority, "default") |
|||
XCTAssertEqual(DailyNotificationConstants.Settings.defaultRetryCount, 3) |
|||
XCTAssertEqual(DailyNotificationConstants.Settings.defaultRetryInterval, 1000) |
|||
} |
|||
} |
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue