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| |
Pod::Spec.new do |s| |
||||
s.name = 'DailyNotificationPlugin' |
s.name = 'DailyNotificationPlugin' |
||||
s.version = '1.0.0' |
s.version = '1.0.0' |
||||
s.summary = 'Daily notification plugin for Capacitor' |
s.summary = 'Daily Notification Plugin for Capacitor' |
||||
s.license = 'MIT' |
s.license = 'MIT' |
||||
s.homepage = 'https://github.com/timesafari/daily-notification-plugin' |
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 = { :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.source_files = 'Plugin/**/*.{swift,h,m,c,cc,mm,cpp}' |
||||
s.ios.deployment_target = '13.0' |
s.ios.deployment_target = '13.0' |
||||
s.dependency 'Capacitor' |
s.dependency 'Capacitor', '~> 5.0.0' |
||||
s.dependency 'CapacitorCordova' |
s.dependency 'CapacitorCordova', '~> 5.0.0' |
||||
s.swift_version = '5.1' |
s.swift_version = '5.1' |
||||
s.module_name = 'DailyNotificationPlugin' |
s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) COCOAPODS=1' } |
||||
|
s.deprecated = false |
||||
s.static_framework = true |
s.static_framework = true |
||||
end |
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 |
import XCTest |
||||
@testable import Plugin |
@testable import DailyNotificationPlugin |
||||
|
|
||||
class DailyNotificationTests: XCTestCase { |
class DailyNotificationTests: XCTestCase { |
||||
var plugin: DailyNotificationPlugin! |
var plugin: DailyNotificationPlugin! |
||||
|
var powerManager: DailyNotificationPowerManager! |
||||
|
var maintenanceWorker: DailyNotificationMaintenanceWorker! |
||||
|
|
||||
override func setUp() { |
override func setUp() { |
||||
super.setUp() |
super.setUp() |
||||
plugin = DailyNotificationPlugin() |
plugin = DailyNotificationPlugin() |
||||
|
powerManager = DailyNotificationPowerManager.shared |
||||
|
maintenanceWorker = DailyNotificationMaintenanceWorker.shared |
||||
|
} |
||||
|
|
||||
|
override func tearDown() { |
||||
|
plugin = nil |
||||
|
super.tearDown() |
||||
} |
} |
||||
|
|
||||
func testTimeValidation() { |
// MARK: - Power Management Tests |
||||
// Valid time |
|
||||
XCTAssertTrue(plugin.isValidTime("09:00")) |
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)") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - Maintenance Tests |
||||
|
|
||||
|
func testMaintenanceTasks() { |
||||
|
// Test cleanup of old notifications |
||||
|
let oldDate = Date().addingTimeInterval(-Double(DailyNotificationConfig.shared.retentionDays * 24 * 60 * 60 + 1)) |
||||
|
|
||||
// Invalid times |
// Create a test notification |
||||
XCTAssertFalse(plugin.isValidTime("25:00")) |
let content = UNMutableNotificationContent() |
||||
XCTAssertFalse(plugin.isValidTime("09:60")) |
content.title = "Test Notification" |
||||
XCTAssertFalse(plugin.isValidTime("9:00")) |
content.body = "This is a test notification" |
||||
XCTAssertFalse(plugin.isValidTime("0900")) |
|
||||
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) |
||||
|
let request = UNNotificationRequest( |
||||
|
identifier: "test-notification", |
||||
|
content: content, |
||||
|
trigger: trigger |
||||
|
) |
||||
|
|
||||
|
let expectation = XCTestExpectation(description: "Cleanup old notifications") |
||||
|
|
||||
|
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() |
||||
|
} |
||||
} |
} |
||||
|
|
||||
func testTimezoneValidation() { |
wait(for: [expectation], timeout: 5.0) |
||||
XCTAssertTrue(plugin.isValidTimezone("America/New_York")) |
|
||||
XCTAssertFalse(plugin.isValidTimezone("Invalid/Timezone")) |
|
||||
} |
} |
||||
|
|
||||
// Add more tests... |
// 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) |
||||
|
} |
||||
|
|
||||
|
// 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