rename 'docs' directory to 'doc'
This commit is contained in:
697
doc/platform/ios/CORE_DATA_MIGRATION.md
Normal file
697
doc/platform/ios/CORE_DATA_MIGRATION.md
Normal file
@@ -0,0 +1,697 @@
|
||||
# iOS Core Data Migration Guide: Android Room → iOS Core Data
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-12-08
|
||||
**Status**: 🎯 **ACTIVE** - Database Migration Reference
|
||||
**Version**: 1.0.0
|
||||
**Last Synced With Plugin Version**: v1.1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
This document provides a comprehensive mapping guide for migrating Android Room database entities to iOS Core Data entities, ensuring cross-platform data consistency and feature parity.
|
||||
|
||||
**Reference**:
|
||||
- [Android Database Schema](../android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt) - Android Room schema
|
||||
- [iOS Core Data Model](../ios/Plugin/DailyNotificationModel.xcdatamodeld) - iOS Core Data model
|
||||
- [Database Consolidation Plan](../android/DATABASE_CONSOLIDATION_PLAN.md) - Unified schema design
|
||||
|
||||
---
|
||||
|
||||
## 1. Entity Mapping Overview
|
||||
|
||||
### 1.1 Complete Entity Mapping
|
||||
|
||||
| Android Room Entity | iOS Core Data Entity | Status | Priority |
|
||||
| ------------------- | -------------------- | ------ | -------- |
|
||||
| `ContentCache` | `ContentCache` | ✅ Implemented | - |
|
||||
| `Schedule` | `Schedule` | ✅ Implemented | - |
|
||||
| `Callback` | `Callback` | ✅ Implemented | - |
|
||||
| `History` | `History` | ✅ Implemented | - |
|
||||
| `NotificationContentEntity` | `NotificationContent` | ❌ Missing | **High** |
|
||||
| `NotificationDeliveryEntity` | `NotificationDelivery` | ❌ Missing | **High** |
|
||||
| `NotificationConfigEntity` | `NotificationConfig` | ❌ Missing | **Medium** |
|
||||
|
||||
### 1.2 Current Implementation Status
|
||||
|
||||
**✅ Implemented (4 entities)**:
|
||||
- `ContentCache` - Fetched content with TTL
|
||||
- `Schedule` - Recurring schedule patterns
|
||||
- `Callback` - Callback configurations
|
||||
- `History` - Execution history
|
||||
|
||||
**❌ Missing (3 entities)**:
|
||||
- `NotificationContent` - Specific notification instances
|
||||
- `NotificationDelivery` - Delivery tracking/analytics
|
||||
- `NotificationConfig` - Configuration management
|
||||
|
||||
---
|
||||
|
||||
## 2. Detailed Entity Mappings
|
||||
|
||||
### 2.1 ContentCache Entity
|
||||
|
||||
**Status**: ✅ **Implemented**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "content_cache")
|
||||
data class ContentCache(
|
||||
@PrimaryKey val id: String,
|
||||
val fetchedAt: Long, // epoch ms
|
||||
val ttlSeconds: Int,
|
||||
val payload: ByteArray, // BLOB
|
||||
val meta: String? = null
|
||||
)
|
||||
```
|
||||
|
||||
**iOS Core Data**:
|
||||
```swift
|
||||
@objc(ContentCache)
|
||||
public class ContentCache: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var fetchedAt: Date?
|
||||
@NSManaged public var ttlSeconds: Int32
|
||||
@NSManaged public var payload: Data?
|
||||
@NSManaged public var meta: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- ✅ All fields mapped correctly
|
||||
- ✅ `Long` (epoch ms) → `Date` conversion handled
|
||||
- ✅ `ByteArray` → `Data` conversion handled
|
||||
- ✅ Optional fields properly marked
|
||||
|
||||
**Migration Status**: ✅ **Complete**
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Schedule Entity
|
||||
|
||||
**Status**: ✅ **Implemented**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "schedules")
|
||||
data class Schedule(
|
||||
@PrimaryKey val id: String,
|
||||
val kind: String, // 'fetch' or 'notify'
|
||||
val cron: String? = null,
|
||||
val clockTime: String? = null, // HH:mm
|
||||
val enabled: Boolean = true,
|
||||
val lastRunAt: Long? = null, // epoch ms
|
||||
val nextRunAt: Long? = null, // epoch ms
|
||||
val jitterMs: Int = 0,
|
||||
val backoffPolicy: String = "exp",
|
||||
val stateJson: String? = null
|
||||
)
|
||||
```
|
||||
|
||||
**iOS Core Data**:
|
||||
```swift
|
||||
@objc(Schedule)
|
||||
public class Schedule: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var kind: String?
|
||||
@NSManaged public var cron: String?
|
||||
@NSManaged public var clockTime: String?
|
||||
@NSManaged public var enabled: Bool
|
||||
@NSManaged public var lastRunAt: Date?
|
||||
@NSManaged public var nextRunAt: Date?
|
||||
@NSManaged public var jitterMs: Int32
|
||||
@NSManaged public var backoffPolicy: String?
|
||||
@NSManaged public var stateJson: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- ✅ All fields mapped correctly
|
||||
- ✅ `Long` (epoch ms) → `Date` conversion handled
|
||||
- ✅ Default values preserved
|
||||
- ✅ Optional fields properly marked
|
||||
|
||||
**Migration Status**: ✅ **Complete**
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Callback Entity
|
||||
|
||||
**Status**: ✅ **Implemented**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "callbacks")
|
||||
data class Callback(
|
||||
@PrimaryKey val id: String,
|
||||
val kind: String, // 'http', 'local', 'queue'
|
||||
val target: String,
|
||||
val headersJson: String? = null,
|
||||
val enabled: Boolean = true,
|
||||
val createdAt: Long // epoch ms
|
||||
)
|
||||
```
|
||||
|
||||
**iOS Core Data**:
|
||||
```swift
|
||||
@objc(Callback)
|
||||
public class Callback: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var kind: String?
|
||||
@NSManaged public var target: String?
|
||||
@NSManaged public var headersJson: String?
|
||||
@NSManaged public var enabled: Bool
|
||||
@NSManaged public var createdAt: Date?
|
||||
}
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- ✅ All fields mapped correctly
|
||||
- ✅ `Long` (epoch ms) → `Date` conversion handled
|
||||
- ✅ Optional fields properly marked
|
||||
|
||||
**Migration Status**: ✅ **Complete**
|
||||
|
||||
---
|
||||
|
||||
### 2.4 History Entity
|
||||
|
||||
**Status**: ✅ **Implemented**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "history")
|
||||
data class History(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
val refId: String,
|
||||
val kind: String, // fetch/notify/callback
|
||||
val occurredAt: Long, // epoch ms
|
||||
val durationMs: Long? = null,
|
||||
val outcome: String,
|
||||
val diagJson: String? = null
|
||||
)
|
||||
```
|
||||
|
||||
**iOS Core Data**:
|
||||
```swift
|
||||
@objc(History)
|
||||
public class History: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var refId: String?
|
||||
@NSManaged public var kind: String?
|
||||
@NSManaged public var occurredAt: Date?
|
||||
@NSManaged public var durationMs: Int32
|
||||
@NSManaged public var outcome: String?
|
||||
@NSManaged public var diagJson: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- ⚠️ `id` type differs: Android uses `Int` (auto-generated), iOS uses `String`
|
||||
- ✅ `Long` (epoch ms) → `Date` conversion handled
|
||||
- ✅ Optional fields properly marked
|
||||
|
||||
**Migration Consideration**: iOS uses `String` for `id` instead of auto-generated `Int`. This is acceptable as long as IDs are generated as UUIDs.
|
||||
|
||||
**Migration Status**: ✅ **Complete** (with note)
|
||||
|
||||
---
|
||||
|
||||
### 2.5 NotificationContent Entity
|
||||
|
||||
**Status**: ❌ **Missing - High Priority**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "notification_content")
|
||||
data class NotificationContentEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val pluginVersion: String?,
|
||||
val timesafariDid: String?,
|
||||
val notificationType: String?,
|
||||
val title: String?,
|
||||
val body: String?,
|
||||
val scheduledTime: Long, // epoch ms
|
||||
val timezone: String?,
|
||||
val priority: Int,
|
||||
val vibrationEnabled: Boolean,
|
||||
val soundEnabled: Boolean,
|
||||
val mediaUrl: String?,
|
||||
val encryptedContent: String?,
|
||||
val encryptionKeyId: String?,
|
||||
val createdAt: Long, // epoch ms
|
||||
val updatedAt: Long, // epoch ms
|
||||
val ttlSeconds: Long,
|
||||
val deliveryStatus: String?,
|
||||
val deliveryAttempts: Int,
|
||||
val lastDeliveryAttempt: Long, // epoch ms
|
||||
val userInteractionCount: Int,
|
||||
val lastUserInteraction: Long, // epoch ms
|
||||
val metadata: String?
|
||||
)
|
||||
```
|
||||
|
||||
**Required iOS Core Data Entity**:
|
||||
```swift
|
||||
@objc(NotificationContent)
|
||||
public class NotificationContent: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var pluginVersion: String?
|
||||
@NSManaged public var timesafariDid: String?
|
||||
@NSManaged public var notificationType: String?
|
||||
@NSManaged public var title: String?
|
||||
@NSManaged public var body: String?
|
||||
@NSManaged public var scheduledTime: Date?
|
||||
@NSManaged public var timezone: String?
|
||||
@NSManaged public var priority: Int32
|
||||
@NSManaged public var vibrationEnabled: Bool
|
||||
@NSManaged public var soundEnabled: Bool
|
||||
@NSManaged public var mediaUrl: String?
|
||||
@NSManaged public var encryptedContent: String?
|
||||
@NSManaged public var encryptionKeyId: String?
|
||||
@NSManaged public var createdAt: Date?
|
||||
@NSManaged public var updatedAt: Date?
|
||||
@NSManaged public var ttlSeconds: Int64
|
||||
@NSManaged public var deliveryStatus: String?
|
||||
@NSManaged public var deliveryAttempts: Int32
|
||||
@NSManaged public var lastDeliveryAttempt: Date?
|
||||
@NSManaged public var userInteractionCount: Int32
|
||||
@NSManaged public var lastUserInteraction: Date?
|
||||
@NSManaged public var metadata: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Required Core Data Model XML**:
|
||||
```xml
|
||||
<entity name="NotificationContent" representedClassName="NotificationContent" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="id" optional="NO" attributeType="String"/>
|
||||
<attribute name="pluginVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="timesafariDid" optional="YES" attributeType="String"/>
|
||||
<attribute name="notificationType" optional="YES" attributeType="String"/>
|
||||
<attribute name="title" optional="YES" attributeType="String"/>
|
||||
<attribute name="body" optional="YES" attributeType="String"/>
|
||||
<attribute name="scheduledTime" optional="NO" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="timezone" optional="YES" attributeType="String"/>
|
||||
<attribute name="priority" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="vibrationEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="soundEnabled" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="mediaUrl" optional="YES" attributeType="String"/>
|
||||
<attribute name="encryptedContent" optional="YES" attributeType="String"/>
|
||||
<attribute name="encryptionKeyId" optional="YES" attributeType="String"/>
|
||||
<attribute name="createdAt" optional="NO" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="updatedAt" optional="NO" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="ttlSeconds" optional="YES" attributeType="Integer 64" defaultValueString="604800" usesScalarValueType="YES"/>
|
||||
<attribute name="deliveryStatus" optional="YES" attributeType="String"/>
|
||||
<attribute name="deliveryAttempts" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastDeliveryAttempt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="userInteractionCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastUserInteraction" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="metadata" optional="YES" attributeType="String"/>
|
||||
<index name="index_notification_content_timesafari_did">
|
||||
<indexElement value="timesafariDid"/>
|
||||
</index>
|
||||
<index name="index_notification_content_notification_type">
|
||||
<indexElement value="notificationType"/>
|
||||
</index>
|
||||
<index name="index_notification_content_scheduled_time">
|
||||
<indexElement value="scheduledTime"/>
|
||||
</index>
|
||||
</entity>
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- `Long` (epoch ms) → `Date` conversion required
|
||||
- `Int` → `Int32` conversion
|
||||
- `Long` (ttlSeconds) → `Int64` conversion
|
||||
- Indexes should be added for performance
|
||||
|
||||
**Migration Status**: ❌ **Not Implemented**
|
||||
|
||||
---
|
||||
|
||||
### 2.6 NotificationDelivery Entity
|
||||
|
||||
**Status**: ❌ **Missing - High Priority**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(
|
||||
tableName = "notification_delivery",
|
||||
foreignKeys = @ForeignKey(
|
||||
entity = NotificationContentEntity::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["notification_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
)
|
||||
data class NotificationDeliveryEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val notificationId: String,
|
||||
val timesafariDid: String?,
|
||||
val deliveryTimestamp: Long, // epoch ms
|
||||
val deliveryStatus: String?,
|
||||
val deliveryMethod: String?,
|
||||
val deliveryAttemptNumber: Int,
|
||||
val deliveryDurationMs: Long,
|
||||
val userInteractionType: String?,
|
||||
val userInteractionTimestamp: Long, // epoch ms
|
||||
val userInteractionDurationMs: Long,
|
||||
val errorCode: String?,
|
||||
val errorMessage: String?,
|
||||
val deviceInfo: String?,
|
||||
val networkInfo: String?,
|
||||
val batteryLevel: Int,
|
||||
val dozeModeActive: Boolean,
|
||||
val exactAlarmPermission: Boolean,
|
||||
val notificationPermission: Boolean,
|
||||
val metadata: String?
|
||||
)
|
||||
```
|
||||
|
||||
**Required iOS Core Data Entity**:
|
||||
```swift
|
||||
@objc(NotificationDelivery)
|
||||
public class NotificationDelivery: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var notificationId: String?
|
||||
@NSManaged public var notificationContent: NotificationContent? // Relationship
|
||||
@NSManaged public var timesafariDid: String?
|
||||
@NSManaged public var deliveryTimestamp: Date?
|
||||
@NSManaged public var deliveryStatus: String?
|
||||
@NSManaged public var deliveryMethod: String?
|
||||
@NSManaged public var deliveryAttemptNumber: Int32
|
||||
@NSManaged public var deliveryDurationMs: Int64
|
||||
@NSManaged public var userInteractionType: String?
|
||||
@NSManaged public var userInteractionTimestamp: Date?
|
||||
@NSManaged public var userInteractionDurationMs: Int64
|
||||
@NSManaged public var errorCode: String?
|
||||
@NSManaged public var errorMessage: String?
|
||||
@NSManaged public var deviceInfo: String?
|
||||
@NSManaged public var networkInfo: String?
|
||||
@NSManaged public var batteryLevel: Int32
|
||||
@NSManaged public var dozeModeActive: Bool
|
||||
@NSManaged public var exactAlarmPermission: Bool
|
||||
@NSManaged public var notificationPermission: Bool
|
||||
@NSManaged public var metadata: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Required Core Data Model XML**:
|
||||
```xml
|
||||
<entity name="NotificationDelivery" representedClassName="NotificationDelivery" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="id" optional="NO" attributeType="String"/>
|
||||
<attribute name="notificationId" optional="YES" attributeType="String"/>
|
||||
<attribute name="timesafariDid" optional="YES" attributeType="String"/>
|
||||
<attribute name="deliveryTimestamp" optional="NO" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="deliveryStatus" optional="YES" attributeType="String"/>
|
||||
<attribute name="deliveryMethod" optional="YES" attributeType="String"/>
|
||||
<attribute name="deliveryAttemptNumber" optional="YES" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
|
||||
<attribute name="deliveryDurationMs" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="userInteractionType" optional="YES" attributeType="String"/>
|
||||
<attribute name="userInteractionTimestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="userInteractionDurationMs" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="errorCode" optional="YES" attributeType="String"/>
|
||||
<attribute name="errorMessage" optional="YES" attributeType="String"/>
|
||||
<attribute name="deviceInfo" optional="YES" attributeType="String"/>
|
||||
<attribute name="networkInfo" optional="YES" attributeType="String"/>
|
||||
<attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="-1" usesScalarValueType="YES"/>
|
||||
<attribute name="dozeModeActive" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="exactAlarmPermission" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="notificationPermission" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="metadata" optional="YES" attributeType="String"/>
|
||||
<relationship name="notificationContent" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="NotificationContent" inverseName="deliveries" inverseEntity="NotificationContent"/>
|
||||
<index name="index_notification_delivery_notification_id">
|
||||
<indexElement value="notificationId"/>
|
||||
</index>
|
||||
<index name="index_notification_delivery_delivery_timestamp">
|
||||
<indexElement value="deliveryTimestamp"/>
|
||||
</index>
|
||||
</entity>
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- Foreign key relationship should be modeled as Core Data relationship
|
||||
- `Long` (epoch ms) → `Date` conversion required
|
||||
- `Int` → `Int32` conversion
|
||||
- `Long` (duration) → `Int64` conversion
|
||||
- Cascade delete should be configured
|
||||
|
||||
**Migration Status**: ❌ **Not Implemented**
|
||||
|
||||
---
|
||||
|
||||
### 2.7 NotificationConfig Entity
|
||||
|
||||
**Status**: ❌ **Missing - Medium Priority**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "notification_config")
|
||||
data class NotificationConfigEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val timesafariDid: String?,
|
||||
val configType: String?,
|
||||
val configKey: String?,
|
||||
val configValue: String?,
|
||||
val configDataType: String?,
|
||||
val isEncrypted: Boolean,
|
||||
val encryptionKeyId: String?,
|
||||
val createdAt: Long, // epoch ms
|
||||
val updatedAt: Long, // epoch ms
|
||||
val ttlSeconds: Long,
|
||||
val isActive: Boolean,
|
||||
val metadata: String?
|
||||
)
|
||||
```
|
||||
|
||||
**Required iOS Core Data Entity**:
|
||||
```swift
|
||||
@objc(NotificationConfig)
|
||||
public class NotificationConfig: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var timesafariDid: String?
|
||||
@NSManaged public var configType: String?
|
||||
@NSManaged public var configKey: String?
|
||||
@NSManaged public var configValue: String?
|
||||
@NSManaged public var configDataType: String?
|
||||
@NSManaged public var isEncrypted: Bool
|
||||
@NSManaged public var encryptionKeyId: String?
|
||||
@NSManaged public var createdAt: Date?
|
||||
@NSManaged public var updatedAt: Date?
|
||||
@NSManaged public var ttlSeconds: Int64
|
||||
@NSManaged public var isActive: Bool
|
||||
@NSManaged public var metadata: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- `Long` (epoch ms) → `Date` conversion required
|
||||
- `Long` (ttlSeconds) → `Int64` conversion
|
||||
- Indexes should be added for performance
|
||||
|
||||
**Migration Status**: ❌ **Not Implemented**
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Type Conversions
|
||||
|
||||
### 3.1 Time Conversions
|
||||
|
||||
**Android → iOS**:
|
||||
- `Long` (epoch milliseconds) → `Date`
|
||||
- Conversion: `Date(timeIntervalSince1970: Double(milliseconds) / 1000.0)`
|
||||
|
||||
**iOS → Android**:
|
||||
- `Date` → `Long` (epoch milliseconds)
|
||||
- Conversion: `Int64(date.timeIntervalSince1970 * 1000)`
|
||||
|
||||
### 3.2 Numeric Conversions
|
||||
|
||||
| Android Type | iOS Type | Notes |
|
||||
| ------------ | -------- | ----- |
|
||||
| `Int` | `Int32` | Direct mapping |
|
||||
| `Long` | `Int64` | For large values |
|
||||
| `Boolean` | `Bool` | Direct mapping |
|
||||
| `ByteArray` | `Data` | Binary data |
|
||||
|
||||
### 3.3 String Conversions
|
||||
|
||||
- `String?` → `String?` (direct mapping)
|
||||
- JSON fields: `String?` → `String?` (parse as needed)
|
||||
|
||||
---
|
||||
|
||||
## 4. Index Mapping
|
||||
|
||||
### 4.1 Required Indexes
|
||||
|
||||
**NotificationContent**:
|
||||
- `timesafariDid` (for user queries)
|
||||
- `notificationType` (for type filtering)
|
||||
- `scheduledTime` (for time-based queries)
|
||||
- `createdAt` (for chronological queries)
|
||||
|
||||
**NotificationDelivery**:
|
||||
- `notificationId` (for foreign key lookups)
|
||||
- `deliveryTimestamp` (for time-based queries)
|
||||
- `deliveryStatus` (for status filtering)
|
||||
- `timesafariDid` (for user queries)
|
||||
|
||||
**NotificationConfig**:
|
||||
- `timesafariDid` (for user queries)
|
||||
- `configType` (for type filtering)
|
||||
- `updatedAt` (for chronological queries)
|
||||
|
||||
---
|
||||
|
||||
## 5. Relationship Mapping
|
||||
|
||||
### 5.1 Foreign Key Relationships
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@ForeignKey(
|
||||
entity = NotificationContentEntity::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["notification_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
```
|
||||
|
||||
**iOS Core Data**:
|
||||
```xml
|
||||
<relationship
|
||||
name="notificationContent"
|
||||
optional="YES"
|
||||
maxCount="1"
|
||||
deletionRule="Cascade"
|
||||
destinationEntity="NotificationContent"
|
||||
inverseName="deliveries"
|
||||
inverseEntity="NotificationContent"/>
|
||||
```
|
||||
|
||||
**Inverse Relationship** (NotificationContent → NotificationDelivery):
|
||||
```xml
|
||||
<relationship
|
||||
name="deliveries"
|
||||
optional="YES"
|
||||
toMany="YES"
|
||||
deletionRule="Nullify"
|
||||
destinationEntity="NotificationDelivery"
|
||||
inverseName="notificationContent"
|
||||
inverseEntity="NotificationDelivery"/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Implementation Checklist
|
||||
|
||||
### 6.1 High Priority (Required for Feature Parity)
|
||||
|
||||
- [ ] Add `NotificationContent` entity to Core Data model
|
||||
- [ ] Add `NotificationDelivery` entity to Core Data model
|
||||
- [ ] Configure foreign key relationship between `NotificationContent` and `NotificationDelivery`
|
||||
- [ ] Add required indexes for performance
|
||||
- [ ] Implement Swift extensions for entity classes
|
||||
- [ ] Add data conversion helpers (Date ↔ Long)
|
||||
- [ ] Test entity creation and relationships
|
||||
- [ ] Test cascade delete behavior
|
||||
|
||||
### 6.2 Medium Priority (Configuration Management)
|
||||
|
||||
- [ ] Add `NotificationConfig` entity to Core Data model
|
||||
- [ ] Add required indexes
|
||||
- [ ] Implement Swift extensions
|
||||
- [ ] Test configuration CRUD operations
|
||||
|
||||
### 6.3 Low Priority (Optimization)
|
||||
|
||||
- [ ] Add migration policies for schema changes
|
||||
- [ ] Add data validation rules
|
||||
- [ ] Optimize fetch requests with predicates
|
||||
- [ ] Add batch operations support
|
||||
|
||||
---
|
||||
|
||||
## 7. Migration Steps
|
||||
|
||||
### 7.1 Step 1: Update Core Data Model
|
||||
|
||||
1. Open `DailyNotificationModel.xcdatamodeld` in Xcode
|
||||
2. Add `NotificationContent` entity with all attributes
|
||||
3. Add `NotificationDelivery` entity with all attributes
|
||||
4. Add `NotificationConfig` entity with all attributes
|
||||
5. Configure relationships between entities
|
||||
6. Add indexes for performance
|
||||
7. Set code generation to "Class Definition"
|
||||
|
||||
### 7.2 Step 2: Create Swift Extensions
|
||||
|
||||
1. Create `NotificationContent+CoreDataClass.swift`
|
||||
2. Create `NotificationContent+CoreDataProperties.swift`
|
||||
3. Create `NotificationDelivery+CoreDataClass.swift`
|
||||
4. Create `NotificationDelivery+CoreDataProperties.swift`
|
||||
5. Create `NotificationConfig+CoreDataClass.swift`
|
||||
6. Create `NotificationConfig+CoreDataProperties.swift`
|
||||
|
||||
### 7.3 Step 3: Implement Data Access Layer
|
||||
|
||||
1. Create DAO classes for each entity
|
||||
2. Implement CRUD operations
|
||||
3. Add data conversion helpers
|
||||
4. Add query methods with predicates
|
||||
|
||||
### 7.4 Step 4: Update Persistence Controller
|
||||
|
||||
1. Update `PersistenceController` to handle new entities
|
||||
2. Add migration policies if needed
|
||||
3. Test database initialization
|
||||
|
||||
### 7.5 Step 5: Testing
|
||||
|
||||
1. Test entity creation
|
||||
2. Test relationships
|
||||
3. Test cascade delete
|
||||
4. Test data conversion (Date ↔ Long)
|
||||
5. Test query performance with indexes
|
||||
|
||||
---
|
||||
|
||||
## 8. Data Migration (If Needed)
|
||||
|
||||
### 8.1 Migration from SQLite to Core Data
|
||||
|
||||
If migrating existing SQLite data to Core Data:
|
||||
|
||||
1. Read data from SQLite database
|
||||
2. Convert data types (Long → Date, etc.)
|
||||
3. Create Core Data entities
|
||||
4. Save to Core Data store
|
||||
5. Verify data integrity
|
||||
|
||||
### 8.2 Migration Script Example
|
||||
|
||||
```swift
|
||||
func migrateSQLiteToCoreData(sqlitePath: String, coreDataStack: NSPersistentContainer) {
|
||||
// 1. Open SQLite database
|
||||
// 2. Query all tables
|
||||
// 3. Convert each row to Core Data entity
|
||||
// 4. Save to Core Data store
|
||||
// 5. Verify migration success
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. References
|
||||
|
||||
- [Android Database Schema](../android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt)
|
||||
- [iOS Core Data Model](../ios/Plugin/DailyNotificationModel.xcdatamodeld)
|
||||
- [Database Consolidation Plan](../android/DATABASE_CONSOLIDATION_PLAN.md)
|
||||
- [Core Data Programming Guide](https://developer.apple.com/documentation/coredata)
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-08
|
||||
**Next Review**: After missing entities are implemented
|
||||
|
||||
658
doc/platform/ios/DOCUMENTATION_REVIEW.md
Normal file
658
doc/platform/ios/DOCUMENTATION_REVIEW.md
Normal file
@@ -0,0 +1,658 @@
|
||||
# iOS Implementation Documentation Review
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-12-08
|
||||
**Status**: 🎯 **ACTIVE** - Documentation Review for iOS Implementation
|
||||
**Purpose**: Ensure Android plugin and test app documentation contains sufficient detail for iOS implementation to mirror all features
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document reviews the Android plugin and test app documentation to ensure that when implementing iOS, there is sufficient information to mirror all Android features. The review identifies:
|
||||
|
||||
1. ✅ **Well-documented features** - Sufficient detail for iOS implementation
|
||||
2. ⚠️ **Partially documented features** - Needs additional detail
|
||||
3. ❌ **Missing documentation** - Critical gaps that need to be addressed
|
||||
|
||||
---
|
||||
|
||||
## 1. Core Architecture Documentation
|
||||
|
||||
### 1.1 Architecture Overview
|
||||
|
||||
**Status**: ✅ **Well Documented**
|
||||
|
||||
**Location**: `ARCHITECTURE.md`
|
||||
|
||||
**Key Information Provided**:
|
||||
- Plugin architecture with component responsibilities
|
||||
- Data architecture with database schema
|
||||
- Storage implementation details
|
||||
- Security architecture
|
||||
- Performance architecture
|
||||
- Migration strategy
|
||||
|
||||
**iOS Implementation Readiness**: ✅ **Ready**
|
||||
- Database schema clearly defined
|
||||
- Component responsibilities well-documented
|
||||
- Storage patterns explained
|
||||
- Security requirements specified
|
||||
|
||||
**Recommendations**:
|
||||
- Add iOS-specific architecture section mapping Android components to iOS equivalents
|
||||
- Document Core Data model mapping to Room schema
|
||||
- Add iOS-specific performance considerations
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Database Schema
|
||||
|
||||
**Status**: ✅ **Well Documented**
|
||||
|
||||
**Location**:
|
||||
- `android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt`
|
||||
- `android/DATABASE_CONSOLIDATION_PLAN.md`
|
||||
- `ARCHITECTURE.md` (Data Architecture section)
|
||||
|
||||
**Key Information Provided**:
|
||||
- Complete database schema with all tables
|
||||
- Field definitions and types
|
||||
- Relationships between entities
|
||||
- Indexing strategy
|
||||
- Migration path
|
||||
|
||||
**Database Tables Documented**:
|
||||
1. ✅ `schedules` - Recurring schedule patterns
|
||||
2. ✅ `content_cache` - Fetched content with TTL
|
||||
3. ✅ `notification_config` - Plugin configuration
|
||||
4. ✅ `callbacks` - Callback configurations
|
||||
5. ✅ `notification_content` - Specific notification instances
|
||||
6. ✅ `notification_delivery` - Delivery tracking
|
||||
7. ✅ `history` - Execution history
|
||||
|
||||
**iOS Implementation Readiness**: ✅ **Ready**
|
||||
- All tables and fields documented
|
||||
- Data types specified
|
||||
- Relationships clear
|
||||
- iOS Core Data model exists (`ios/Plugin/DailyNotificationModel.xcdatamodeld`)
|
||||
|
||||
**Recommendations**:
|
||||
- Add iOS Core Data entity mapping document
|
||||
- Document iOS-specific storage considerations (UserDefaults vs Core Data)
|
||||
- Add migration guide from Android Room to iOS Core Data
|
||||
|
||||
---
|
||||
|
||||
## 2. Recovery Scenarios Documentation
|
||||
|
||||
### 2.1 Recovery Scenarios Overview
|
||||
|
||||
**Status**: ✅ **Well Documented**
|
||||
|
||||
**Location**:
|
||||
- `doc/android-implementation-directive.md`
|
||||
- `doc/android-implementation-directive-phase1.md`
|
||||
- `doc/android-implementation-directive-phase2.md`
|
||||
- `doc/android-implementation-directive-phase3.md`
|
||||
- `android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt`
|
||||
|
||||
**Recovery Scenarios Documented**:
|
||||
|
||||
1. ✅ **COLD_START** - Process killed, alarms may or may not exist
|
||||
- Detection logic documented
|
||||
- Recovery steps specified
|
||||
- Logging requirements defined
|
||||
|
||||
2. ✅ **FORCE_STOP** - Alarms cleared, DB still populated
|
||||
- Detection logic documented
|
||||
- Recovery steps specified
|
||||
- Android-specific (not applicable to iOS)
|
||||
|
||||
3. ✅ **BOOT** - Device reboot
|
||||
- Detection logic documented
|
||||
- Recovery steps specified
|
||||
- Boot receiver implementation detailed
|
||||
|
||||
4. ✅ **NONE** - No recovery required (warm resume or first launch)
|
||||
- Detection logic documented
|
||||
- Behavior specified
|
||||
|
||||
**iOS Implementation Readiness**: ⚠️ **Partially Ready**
|
||||
|
||||
**Gaps Identified**:
|
||||
- iOS doesn't have "force stop" equivalent - need to document iOS app termination scenarios
|
||||
- iOS boot recovery uses different mechanism (BGTaskScheduler registration)
|
||||
- iOS warm/cold start detection differs from Android
|
||||
|
||||
**Recommendations**:
|
||||
- Add iOS-specific recovery scenario mapping:
|
||||
- Android `FORCE_STOP` → iOS "App Terminated by System"
|
||||
- Android `BOOT` → iOS "Device Reboot" (BGTaskScheduler)
|
||||
- Android `COLD_START` → iOS "App Launch After Termination"
|
||||
- Document iOS-specific detection mechanisms
|
||||
- Add iOS recovery implementation guide
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Recovery Implementation Details
|
||||
|
||||
**Status**: ✅ **Well Documented**
|
||||
|
||||
**Location**: `android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt`
|
||||
|
||||
**Key Implementation Details Documented**:
|
||||
- Scenario detection algorithm
|
||||
- Alarm existence checking
|
||||
- Boot flag management
|
||||
- Recovery result tracking
|
||||
- Error handling
|
||||
- Timeout protection
|
||||
|
||||
**Code Documentation Quality**: ✅ **Excellent**
|
||||
- Comprehensive inline comments
|
||||
- Method-level documentation
|
||||
- Parameter descriptions
|
||||
- Return value documentation
|
||||
- Error handling documented
|
||||
|
||||
**iOS Implementation Readiness**: ✅ **Ready**
|
||||
- Algorithm logic clear
|
||||
- Edge cases documented
|
||||
- Error handling patterns specified
|
||||
|
||||
**Recommendations**:
|
||||
- Add iOS-specific implementation notes:
|
||||
- iOS alarm checking (UNUserNotificationCenter.getPendingNotificationRequests)
|
||||
- iOS boot detection (BGTaskScheduler registration)
|
||||
- iOS app termination detection (applicationWillTerminate)
|
||||
|
||||
---
|
||||
|
||||
## 3. Plugin Methods Documentation
|
||||
|
||||
### 3.1 Plugin API Methods
|
||||
|
||||
**Status**: ⚠️ **Partially Documented**
|
||||
|
||||
**Location**:
|
||||
- `API.md`
|
||||
- `README.md`
|
||||
- `android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt`
|
||||
|
||||
**Methods Documented**: 54 `@PluginMethod` annotations found
|
||||
|
||||
**Well-Documented Methods**:
|
||||
- ✅ `scheduleDailyNotification` - API.md, README.md
|
||||
- ✅ `scheduleDailyReminder` - API.md, README.md
|
||||
- ✅ `getScheduledReminders` - API.md, README.md
|
||||
- ✅ `cancelDailyReminder` - API.md, README.md
|
||||
- ✅ `isAlarmScheduled` - API.md (Android-specific)
|
||||
- ✅ `getNextAlarmTime` - API.md (Android-specific)
|
||||
- ✅ `testAlarm` - API.md (Android-specific)
|
||||
|
||||
**Partially Documented Methods**:
|
||||
- ⚠️ Database CRUD methods - Some documented in `doc/architecture/DATABASE_INTERFACES.md`
|
||||
- ⚠️ Configuration methods - Limited documentation
|
||||
- ⚠️ History/analytics methods - Limited documentation
|
||||
|
||||
**Missing Documentation**:
|
||||
- ❌ Complete method signature list with parameters
|
||||
- ❌ Return value specifications
|
||||
- ❌ Error conditions and codes
|
||||
- ❌ Platform-specific behavior differences
|
||||
|
||||
**iOS Implementation Readiness**: ⚠️ **Partially Ready**
|
||||
|
||||
**Recommendations**:
|
||||
- Create comprehensive API reference document with:
|
||||
- All 54 methods listed
|
||||
- Complete parameter specifications
|
||||
- Return value types
|
||||
- Error conditions
|
||||
- Platform-specific notes
|
||||
- Add iOS-specific method documentation
|
||||
- Document which methods are Android-only vs cross-platform
|
||||
|
||||
---
|
||||
|
||||
## 4. Testing Documentation
|
||||
|
||||
### 4.1 Test Scripts
|
||||
|
||||
**Status**: ✅ **Well Documented**
|
||||
|
||||
**Location**:
|
||||
- `test-apps/android-test-app/test-phase1.sh`
|
||||
- `test-apps/android-test-app/test-phase2.sh`
|
||||
- `test-apps/android-test-app/test-phase3.sh`
|
||||
|
||||
**Test Coverage Documented**:
|
||||
|
||||
**Phase 1 Tests**:
|
||||
- ✅ TEST 0: Daily Rollover
|
||||
- ✅ TEST 1: Cold Start Recovery
|
||||
- ✅ TEST 2: Alarm Persistence (Swipe from Recents)
|
||||
- ✅ TEST 3: Invalid Data Handling
|
||||
|
||||
**Phase 2 Tests**:
|
||||
- ✅ TEST 1: Force Stop with Cleared Alarms
|
||||
- ✅ TEST 2: Alarms Intact (Warm Resume)
|
||||
- ✅ TEST 3: Multiple Schedules Recovery
|
||||
|
||||
**Phase 3 Tests**:
|
||||
- ✅ TEST 1: Boot with Future Alarms
|
||||
- ✅ TEST 2: Boot with Past Alarms
|
||||
- ✅ TEST 3: Boot Recovery Idempotency
|
||||
- ✅ TEST 4: Boot Recovery Without App Launch
|
||||
|
||||
**Test Script Quality**: ✅ **Excellent**
|
||||
- Clear test procedures
|
||||
- Expected results specified
|
||||
- Verification steps documented
|
||||
- Error handling included
|
||||
|
||||
**iOS Implementation Readiness**: ⚠️ **Partially Ready**
|
||||
|
||||
**Gaps Identified**:
|
||||
- Test scripts are Android-specific (ADB commands)
|
||||
- iOS testing requires different tools (xcrun simctl, etc.)
|
||||
- Some tests are Android-only (force stop)
|
||||
|
||||
**Recommendations**:
|
||||
- Create iOS test script equivalents:
|
||||
- `test-apps/ios-test-app/test-phase1.sh`
|
||||
- `test-apps/ios-test-app/test-phase2.sh`
|
||||
- `test-apps/ios-test-app/test-phase3.sh`
|
||||
- Document iOS-specific test procedures
|
||||
- Map Android tests to iOS equivalents
|
||||
- Document iOS testing tools and commands
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Test Documentation
|
||||
|
||||
**Status**: ✅ **Well Documented**
|
||||
|
||||
**Location**:
|
||||
- `doc/alarms/PHASE1-VERIFICATION.md`
|
||||
- `doc/alarms/PHASE2-VERIFICATION.md`
|
||||
- `doc/alarms/PHASE3-VERIFICATION.md`
|
||||
- `doc/alarms/PHASE1-EMULATOR-TESTING.md`
|
||||
- `doc/alarms/PHASE2-EMULATOR-TESTING.md`
|
||||
- `doc/alarms/PHASE3-EMULATOR-TESTING.md`
|
||||
|
||||
**Documentation Quality**: ✅ **Excellent**
|
||||
- Test procedures clearly defined
|
||||
- Expected results specified
|
||||
- Verification steps documented
|
||||
- Troubleshooting guides included
|
||||
|
||||
**iOS Implementation Readiness**: ⚠️ **Partially Ready**
|
||||
|
||||
**Recommendations**:
|
||||
- Create iOS verification documents
|
||||
- Document iOS simulator testing procedures
|
||||
- Add iOS-specific troubleshooting guides
|
||||
|
||||
---
|
||||
|
||||
## 5. Platform-Specific Features
|
||||
|
||||
### 5.1 Android-Specific Features
|
||||
|
||||
**Status**: ✅ **Well Documented**
|
||||
|
||||
**Features Documented**:
|
||||
- ✅ AlarmManager integration
|
||||
- ✅ BootReceiver implementation
|
||||
- ✅ WorkManager for background tasks
|
||||
- ✅ Exact alarm permissions
|
||||
- ✅ Notification channels
|
||||
- ✅ SharedPreferences for flags
|
||||
|
||||
**iOS Implementation Readiness**: ✅ **Ready**
|
||||
- Android features clearly documented
|
||||
- iOS equivalents identified in requirements
|
||||
|
||||
**Recommendations**:
|
||||
- Add iOS feature mapping document:
|
||||
- AlarmManager → UNUserNotificationCenter
|
||||
- BootReceiver → BGTaskScheduler
|
||||
- WorkManager → BGTaskScheduler
|
||||
- Exact alarm permissions → Notification permissions
|
||||
- Notification channels → Notification categories
|
||||
|
||||
---
|
||||
|
||||
### 5.2 iOS-Specific Considerations
|
||||
|
||||
**Status**: ❌ **Missing**
|
||||
|
||||
**Gaps Identified**:
|
||||
- No iOS-specific implementation guide
|
||||
- No iOS platform capability reference
|
||||
- No iOS testing procedures
|
||||
- No iOS troubleshooting guide
|
||||
|
||||
**Recommendations**:
|
||||
- Create `doc/ios-implementation-directive.md`
|
||||
- Create `doc/ios-platform-capability-reference.md`
|
||||
- Create iOS testing procedures
|
||||
- Create iOS troubleshooting guide
|
||||
|
||||
---
|
||||
|
||||
## 6. Data Models and Entities
|
||||
|
||||
### 6.1 Schedule Entity
|
||||
|
||||
**Status**: ✅ **Well Documented**
|
||||
|
||||
**Location**: `android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt`
|
||||
|
||||
**Fields Documented**:
|
||||
- ✅ `id` - String, PrimaryKey
|
||||
- ✅ `kind` - String ('fetch' or 'notify')
|
||||
- ✅ `cron` - String? (optional cron expression)
|
||||
- ✅ `clockTime` - String? (optional HH:mm)
|
||||
- ✅ `enabled` - Boolean
|
||||
- ✅ `lastRunAt` - Long? (epoch ms)
|
||||
- ✅ `nextRunAt` - Long? (epoch ms)
|
||||
- ✅ `jitterMs` - Int
|
||||
- ✅ `backoffPolicy` - String
|
||||
- ✅ `stateJson` - String?
|
||||
|
||||
**iOS Implementation Readiness**: ✅ **Ready**
|
||||
- All fields documented
|
||||
- Types specified
|
||||
- iOS Core Data model exists
|
||||
|
||||
---
|
||||
|
||||
### 6.2 NotificationContentEntity
|
||||
|
||||
**Status**: ✅ **Well Documented**
|
||||
|
||||
**Location**: `android/src/main/java/com/timesafari/dailynotification/entities/NotificationContentEntity.java`
|
||||
|
||||
**Fields Documented**: All fields with types and purposes
|
||||
|
||||
**iOS Implementation Readiness**: ✅ **Ready**
|
||||
|
||||
---
|
||||
|
||||
### 6.3 Other Entities
|
||||
|
||||
**Status**: ✅ **Well Documented**
|
||||
|
||||
All entities documented in database schema
|
||||
|
||||
---
|
||||
|
||||
## 7. Recovery Logic Details
|
||||
|
||||
### 7.1 Scenario Detection Algorithm
|
||||
|
||||
**Status**: ✅ **Well Documented**
|
||||
|
||||
**Location**: `android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt`
|
||||
|
||||
**Algorithm Documented**:
|
||||
```kotlin
|
||||
1. Check if database has schedules
|
||||
2. If empty → NONE
|
||||
3. Check if alarms exist in AlarmManager
|
||||
4. If no alarms:
|
||||
- Check boot flag (recent within 60s) → BOOT
|
||||
- Otherwise → FORCE_STOP
|
||||
5. If alarms exist:
|
||||
- Check boot flag → BOOT (if set)
|
||||
- Otherwise → COLD_START
|
||||
```
|
||||
|
||||
**iOS Implementation Readiness**: ✅ **Ready**
|
||||
- Algorithm logic clear
|
||||
- Edge cases documented
|
||||
|
||||
**Recommendations**:
|
||||
- Add iOS-specific detection notes:
|
||||
- iOS alarm checking method
|
||||
- iOS boot detection mechanism
|
||||
- iOS app termination detection
|
||||
|
||||
---
|
||||
|
||||
### 7.2 Recovery Execution
|
||||
|
||||
**Status**: ✅ **Well Documented**
|
||||
|
||||
**Recovery Steps Documented**:
|
||||
- ✅ Missed alarm detection
|
||||
- ✅ Alarm rescheduling
|
||||
- ✅ Error handling
|
||||
- ✅ History recording
|
||||
- ✅ Timeout protection
|
||||
|
||||
**iOS Implementation Readiness**: ✅ **Ready**
|
||||
|
||||
---
|
||||
|
||||
## 8. Boot Recovery
|
||||
|
||||
### 8.1 BootReceiver Implementation
|
||||
|
||||
**Status**: ✅ **Well Documented**
|
||||
|
||||
**Location**:
|
||||
- `android/src/main/java/com/timesafari/dailynotification/BootReceiver.kt`
|
||||
- `doc/android-implementation-directive-phase3.md`
|
||||
|
||||
**Documentation Includes**:
|
||||
- ✅ Boot receiver registration
|
||||
- ✅ Intent filter configuration
|
||||
- ✅ Recovery logic
|
||||
- ✅ Boot flag management
|
||||
- ✅ Error handling
|
||||
|
||||
**iOS Implementation Readiness**: ⚠️ **Partially Ready**
|
||||
|
||||
**Gaps Identified**:
|
||||
- iOS uses BGTaskScheduler, not broadcast receiver
|
||||
- iOS boot detection mechanism differs
|
||||
- iOS background task registration required
|
||||
|
||||
**Recommendations**:
|
||||
- Document iOS BGTaskScheduler registration
|
||||
- Document iOS boot detection mechanism
|
||||
- Add iOS boot recovery implementation guide
|
||||
|
||||
---
|
||||
|
||||
## 9. Critical Documentation Gaps for iOS
|
||||
|
||||
### 9.1 Missing Documentation
|
||||
|
||||
1. ❌ **iOS Platform Capability Reference**
|
||||
- Need: Document iOS-specific OS behaviors
|
||||
- Location: `doc/ios-platform-capability-reference.md`
|
||||
|
||||
2. ❌ **iOS Implementation Directive**
|
||||
- Need: Step-by-step iOS implementation guide
|
||||
- Location: `doc/ios-implementation-directive.md`
|
||||
|
||||
3. ❌ **iOS Test Scripts**
|
||||
- Need: iOS test script equivalents
|
||||
- Location: `test-apps/ios-test-app/test-phase*.sh`
|
||||
|
||||
4. ❌ **iOS API Method Documentation**
|
||||
- Need: Complete iOS method signatures
|
||||
- Location: `API.md` (iOS section)
|
||||
|
||||
5. ❌ **iOS Recovery Scenario Mapping**
|
||||
- Need: Android → iOS scenario mapping
|
||||
- Location: `doc/ios-recovery-scenario-mapping.md`
|
||||
|
||||
6. ❌ **iOS Core Data Migration Guide**
|
||||
- Need: Room → Core Data migration guide
|
||||
- Location: `doc/ios-core-data-migration.md`
|
||||
|
||||
---
|
||||
|
||||
### 9.2 Partially Documented Areas
|
||||
|
||||
1. ⚠️ **Plugin Methods**
|
||||
- Status: 54 methods found, ~20 documented in API.md
|
||||
- Need: Complete method reference
|
||||
|
||||
2. ⚠️ **Error Handling**
|
||||
- Status: Some error codes documented
|
||||
- Need: Complete error code reference
|
||||
|
||||
3. ⚠️ **Platform Differences**
|
||||
- Status: Some differences noted
|
||||
- Need: Comprehensive platform comparison
|
||||
|
||||
---
|
||||
|
||||
## 10. Recommendations Summary
|
||||
|
||||
### 10.1 High Priority
|
||||
|
||||
1. **Create iOS Platform Capability Reference**
|
||||
- Document iOS-specific OS behaviors
|
||||
- Map Android behaviors to iOS equivalents
|
||||
|
||||
2. **Create iOS Implementation Directive**
|
||||
- Step-by-step implementation guide
|
||||
- Mirror Android phase structure
|
||||
|
||||
3. **Complete API Documentation**
|
||||
- Document all 54 plugin methods
|
||||
- Add iOS-specific method notes
|
||||
|
||||
4. **Create iOS Test Scripts**
|
||||
- Mirror Android test structure
|
||||
- Use iOS testing tools
|
||||
|
||||
### 10.2 Medium Priority
|
||||
|
||||
1. **Add iOS Recovery Scenario Mapping**
|
||||
- Map Android scenarios to iOS
|
||||
- Document iOS-specific scenarios
|
||||
|
||||
2. **Create iOS Core Data Migration Guide**
|
||||
- Room → Core Data mapping
|
||||
- Data migration procedures
|
||||
|
||||
3. **Add iOS Troubleshooting Guide**
|
||||
- Common iOS issues
|
||||
- Debugging procedures
|
||||
|
||||
### 10.3 Low Priority
|
||||
|
||||
1. **Add iOS Architecture Diagrams**
|
||||
- Visual component mapping
|
||||
- iOS-specific architecture notes
|
||||
|
||||
2. **Create iOS Performance Guide**
|
||||
- iOS-specific optimizations
|
||||
- Battery considerations
|
||||
|
||||
---
|
||||
|
||||
## 11. Documentation Quality Assessment
|
||||
|
||||
### 11.1 Overall Assessment
|
||||
|
||||
**Android Documentation Quality**: ✅ **Excellent**
|
||||
- Comprehensive coverage
|
||||
- Clear structure
|
||||
- Good code examples
|
||||
- Well-organized
|
||||
|
||||
**iOS Implementation Readiness**: ⚠️ **Partially Ready**
|
||||
- Core architecture: ✅ Ready
|
||||
- Database schema: ✅ Ready
|
||||
- Recovery logic: ✅ Ready
|
||||
- Platform specifics: ❌ Missing
|
||||
- Testing: ⚠️ Partially ready
|
||||
|
||||
### 11.2 Strengths
|
||||
|
||||
1. ✅ **Database schema well-documented**
|
||||
2. ✅ **Recovery scenarios clearly defined**
|
||||
3. ✅ **Test procedures comprehensive**
|
||||
4. ✅ **Code documentation excellent**
|
||||
5. ✅ **Architecture clearly explained**
|
||||
|
||||
### 11.3 Weaknesses
|
||||
|
||||
1. ❌ **Missing iOS-specific documentation**
|
||||
2. ⚠️ **Incomplete API method documentation**
|
||||
3. ⚠️ **Platform differences not fully documented**
|
||||
4. ❌ **No iOS test scripts**
|
||||
|
||||
---
|
||||
|
||||
## 12. Action Items
|
||||
|
||||
### 12.1 For iOS Implementation
|
||||
|
||||
1. **Before Starting Implementation**:
|
||||
- [ ] Review Android architecture documentation
|
||||
- [ ] Review database schema
|
||||
- [ ] Review recovery scenarios
|
||||
- [ ] Review test procedures
|
||||
|
||||
2. **During Implementation**:
|
||||
- [ ] Create iOS platform capability reference
|
||||
- [ ] Document iOS-specific behaviors
|
||||
- [ ] Create iOS test scripts
|
||||
- [ ] Document iOS platform differences
|
||||
|
||||
3. **After Implementation**:
|
||||
- [ ] Update API documentation with iOS notes
|
||||
- [ ] Create iOS troubleshooting guide
|
||||
- [ ] Document iOS-specific optimizations
|
||||
|
||||
### 12.2 For Documentation Improvement
|
||||
|
||||
1. **Complete API Documentation**:
|
||||
- [ ] Document all 54 plugin methods
|
||||
- [ ] Add parameter specifications
|
||||
- [ ] Add return value types
|
||||
- [ ] Add error conditions
|
||||
|
||||
2. **Add iOS Documentation**:
|
||||
- [ ] Create iOS platform capability reference
|
||||
- [ ] Create iOS implementation directive
|
||||
- [ ] Create iOS test scripts
|
||||
- [ ] Create iOS troubleshooting guide
|
||||
|
||||
3. **Improve Cross-Platform Documentation**:
|
||||
- [ ] Add platform comparison matrix
|
||||
- [ ] Document platform-specific features
|
||||
- [ ] Add migration guides
|
||||
|
||||
---
|
||||
|
||||
## 13. Conclusion
|
||||
|
||||
The Android plugin and test app documentation is **comprehensive and well-structured**. The core architecture, database schema, and recovery logic are **sufficiently documented** for iOS implementation to mirror Android features.
|
||||
|
||||
**Key Findings**:
|
||||
- ✅ Core architecture: Ready for iOS implementation
|
||||
- ✅ Database schema: Ready for iOS implementation
|
||||
- ✅ Recovery logic: Ready for iOS implementation
|
||||
- ❌ Platform specifics: Missing iOS documentation
|
||||
- ⚠️ API methods: Partially documented
|
||||
|
||||
**Recommendation**: Proceed with iOS implementation using existing Android documentation, while creating iOS-specific documentation as needed. The Android documentation provides a solid foundation for iOS implementation.
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-08
|
||||
**Next Review**: After iOS implementation begins
|
||||
|
||||
395
doc/platform/ios/IMPLEMENTATION_DIRECTIVE.md
Normal file
395
doc/platform/ios/IMPLEMENTATION_DIRECTIVE.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# iOS Implementation Directive: App Launch Recovery & Missed Notification Detection
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-12-08
|
||||
**Status**: Active Implementation Directive - iOS Only
|
||||
**Version**: 1.0.0
|
||||
**Last Synced With Plugin Version**: v1.1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
This directive provides **descriptive overview and integration guidance** for iOS-specific recovery and missed notification detection:
|
||||
|
||||
1. App Launch Recovery (cold/warm/terminated)
|
||||
2. Missed Notification Detection
|
||||
3. App Termination Detection
|
||||
4. Background Task Registration for Boot Recovery
|
||||
|
||||
**⚠️ CRITICAL**: This document is **descriptive and integrative**. The **normative implementation instructions** are in the Phase 1–3 directives below. **If any code or behavior in this file conflicts with a Phase directive, the Phase directive wins.**
|
||||
|
||||
**Reference**: See [Plugin Requirements](./alarms/03-plugin-requirements.md) for requirements that Phase directives implement.
|
||||
|
||||
**Reference**: See [Platform Capability Reference](./alarms/01-platform-capability-reference.md) for iOS OS-level facts.
|
||||
|
||||
**⚠️ IMPORTANT**: For implementation, use the phase-specific directives (these are the canonical source of truth):
|
||||
|
||||
- **[Phase 1: Cold Start Recovery](./ios-implementation-directive-phase1.md)** - Minimal viable recovery
|
||||
- Implements: [Plugin Requirements §3.1.2](./alarms/03-plugin-requirements.md#312-app-cold-start) (iOS equivalent)
|
||||
- Explicit acceptance criteria, rollback safety, data integrity checks
|
||||
- **Start here** for fastest implementation
|
||||
|
||||
- **[Phase 2: App Termination Detection & Recovery](./ios-implementation-directive-phase2.md)** - Comprehensive termination handling
|
||||
- Implements: iOS-specific app termination scenarios
|
||||
- Prerequisite: Phase 1 complete
|
||||
|
||||
- **[Phase 3: Background Task Registration & Boot Recovery](./ios-implementation-directive-phase3.md)** - Background task enhancement
|
||||
- Implements: BGTaskScheduler registration for boot recovery
|
||||
- Prerequisites: Phase 1 and Phase 2 complete
|
||||
|
||||
**See Also**: [Unified Alarm Directive](./alarms/000-UNIFIED-ALARM-DIRECTIVE.md) for master coordination document.
|
||||
|
||||
---
|
||||
|
||||
## 1. Implementation Overview
|
||||
|
||||
### 1.1 What Needs to Be Implemented
|
||||
|
||||
| Feature | Status | Priority | Location |
|
||||
| ------- | ------ | -------- | -------- |
|
||||
| App Launch Recovery | ❌ Missing | **High** | `DailyNotificationPlugin.swift` - `load()` method |
|
||||
| Missed Notification Detection | ⚠️ Partial | **High** | `DailyNotificationPlugin.swift` - new method |
|
||||
| App Termination Detection | ❌ Missing | **High** | `DailyNotificationPlugin.swift` - recovery logic |
|
||||
| Background Task Registration | ⚠️ Partial | **Medium** | `AppDelegate.swift` - BGTaskScheduler registration |
|
||||
|
||||
### 1.2 Implementation Strategy
|
||||
|
||||
**Phase 1** – Cold start recovery only
|
||||
- Missed notification detection + future notification verification
|
||||
- No termination detection, no boot handling
|
||||
- **See [Phase 1 directive](./ios-implementation-directive-phase1.md) for implementation**
|
||||
|
||||
**Phase 2** – App termination detection & full recovery
|
||||
- Termination detection via UNUserNotificationCenter state comparison
|
||||
- Comprehensive recovery of all schedules (notify + fetch)
|
||||
- Past notifications marked as missed, future notifications rescheduled
|
||||
- **See [Phase 2 directive](./ios-implementation-directive-phase2.md) for implementation**
|
||||
|
||||
**Phase 3** – Background task registration & boot recovery
|
||||
- BGTaskScheduler registration for boot recovery
|
||||
- Next occurrence rescheduled for repeating schedules
|
||||
- **See [Phase 3 directive](./ios-implementation-directive-phase3.md) for implementation**
|
||||
|
||||
---
|
||||
|
||||
## 2. iOS-Specific Considerations
|
||||
|
||||
### 2.1 Key Differences from Android
|
||||
|
||||
**iOS Advantages**:
|
||||
- ✅ Notifications persist across app termination (OS-guaranteed)
|
||||
- ✅ Notifications persist across device reboot (OS-guaranteed)
|
||||
- ✅ No force stop equivalent (iOS doesn't have user-facing force stop)
|
||||
|
||||
**iOS Challenges**:
|
||||
- ❌ App code does NOT run when notification fires (only if user taps)
|
||||
- ❌ Background execution severely limited (BGTaskScheduler only)
|
||||
- ❌ Cannot rely on background execution for recovery
|
||||
- ❌ Must detect missed notifications on app launch
|
||||
|
||||
**Platform Reference**: See [Platform Capability Reference §3](./alarms/01-platform-capability-reference.md#3-ios-notification-capability-matrix) for complete iOS behavior matrix.
|
||||
|
||||
### 2.2 Recovery Scenario Mapping
|
||||
|
||||
**Android → iOS Mapping**:
|
||||
|
||||
| Android Scenario | iOS Equivalent | Detection Method |
|
||||
| ---------------- | -------------- | --------------- |
|
||||
| `COLD_START` | App Launch After Termination | Check if notifications exist vs DB state |
|
||||
| `FORCE_STOP` | App Terminated by System | Check if notifications missing vs DB state |
|
||||
| `BOOT` | Device Reboot | BGTaskScheduler registration (Phase 3) |
|
||||
| `WARM_START` | App Resume (Foreground) | Check app state on resume |
|
||||
|
||||
**Note**: iOS doesn't have a user-facing "force stop" equivalent. System termination is detected by comparing UNUserNotificationCenter state with database state.
|
||||
|
||||
### 2.3 iOS APIs Used
|
||||
|
||||
**Notification Management**:
|
||||
- `UNUserNotificationCenter.current()` - Notification center
|
||||
- `UNUserNotificationCenter.getPendingNotificationRequests()` - Check scheduled notifications
|
||||
- `UNUserNotificationCenter.add()` - Schedule notifications
|
||||
|
||||
**Background Tasks**:
|
||||
- `BGTaskScheduler.shared` - Background task scheduler
|
||||
- `BGTaskScheduler.register()` - Register background task handlers
|
||||
- `BGAppRefreshTaskRequest` - Background fetch requests
|
||||
|
||||
**App Lifecycle**:
|
||||
- `applicationWillTerminate` - App termination notification
|
||||
- `applicationDidBecomeActive` - App foreground notification
|
||||
- `applicationDidEnterBackground` - App background notification
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation: ReactivationManager (iOS)
|
||||
|
||||
**⚠️ Illustrative only** – See Phase 1 and Phase 2 directives for canonical implementation.
|
||||
|
||||
**ReactivationManager Responsibilities by Phase**:
|
||||
|
||||
| Phase | Responsibilities |
|
||||
| ----- | ---------------- |
|
||||
| 1 | Cold start only (missed detection + verify/reschedule future) |
|
||||
| 2 | Adds termination detection & recovery |
|
||||
| 3 | Background task registration & boot recovery |
|
||||
|
||||
**For implementation details, see**:
|
||||
- [Phase 1: ReactivationManager creation](./ios-implementation-directive-phase1.md#2-implementation-reactivationmanager)
|
||||
- [Phase 2: Termination detection](./ios-implementation-directive-phase2.md#2-implementation-termination-detection)
|
||||
|
||||
### 3.1 Create New File
|
||||
|
||||
**File**: `ios/Plugin/DailyNotificationReactivationManager.swift`
|
||||
|
||||
**Purpose**: Centralized recovery logic for app launch scenarios
|
||||
|
||||
### 3.2 Class Structure
|
||||
|
||||
**⚠️ Illustrative only** – See Phase 1 for canonical implementation.
|
||||
|
||||
```swift
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
|
||||
/**
|
||||
* Manages recovery of notifications on app launch
|
||||
* Handles cold start, warm start, and termination recovery scenarios
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class DailyNotificationReactivationManager {
|
||||
|
||||
private static let TAG = "DNP-REACTIVATION"
|
||||
private let notificationCenter = UNUserNotificationCenter.current()
|
||||
private let database: DailyNotificationDatabase
|
||||
private let storage: DailyNotificationStorage
|
||||
|
||||
init(database: DailyNotificationDatabase, storage: DailyNotificationStorage) {
|
||||
self.database = database
|
||||
self.storage = storage
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform recovery on app launch
|
||||
* Detects scenario (cold/warm/termination) and handles accordingly
|
||||
*/
|
||||
func performRecovery() async {
|
||||
do {
|
||||
NSLog("\(Self.TAG): Starting app launch recovery")
|
||||
|
||||
// Step 1: Detect scenario
|
||||
let scenario = try await detectScenario()
|
||||
NSLog("\(Self.TAG): Detected scenario: \(scenario)")
|
||||
|
||||
// Step 2: Handle based on scenario
|
||||
switch scenario {
|
||||
case .termination:
|
||||
try await handleTerminationRecovery()
|
||||
case .coldStart:
|
||||
try await handleColdStartRecovery()
|
||||
case .warmStart:
|
||||
try await handleWarmStartRecovery()
|
||||
case .none:
|
||||
NSLog("\(Self.TAG): No recovery needed")
|
||||
}
|
||||
|
||||
NSLog("\(Self.TAG): App launch recovery completed")
|
||||
} catch {
|
||||
NSLog("\(Self.TAG): Error during app launch recovery: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// ... implementation methods below ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Recovery Scenario Detection
|
||||
|
||||
### 4.1 Scenario Detection Algorithm
|
||||
|
||||
**Platform Reference**: [iOS §3.1.1](./alarms/01-platform-capability-reference.md#311-notifications-survive-app-termination) - Notifications survive app termination
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
```swift
|
||||
enum RecoveryScenario {
|
||||
case none // No recovery needed (first launch or warm resume)
|
||||
case coldStart // App launched after termination, notifications may exist
|
||||
case termination // App terminated, notifications missing vs DB
|
||||
case warmStart // App resumed from background (optimization only)
|
||||
}
|
||||
|
||||
func detectScenario() async throws -> RecoveryScenario {
|
||||
// Step 1: Check if database has schedules
|
||||
let schedules = try database.getEnabledSchedules()
|
||||
if schedules.isEmpty {
|
||||
return .none // First launch
|
||||
}
|
||||
|
||||
// Step 2: Get pending notifications from UNUserNotificationCenter
|
||||
let pendingNotifications = try await notificationCenter.pendingNotificationRequests()
|
||||
|
||||
// Step 3: Compare DB state with notification center state
|
||||
let dbNotificationIds = Set(schedules.flatMap { $0.getScheduledNotificationIds() })
|
||||
let pendingIds = Set(pendingNotifications.map { $0.identifier })
|
||||
|
||||
// Step 4: Determine scenario
|
||||
if pendingIds.isEmpty && !dbNotificationIds.isEmpty {
|
||||
// DB has schedules but no notifications scheduled
|
||||
return .termination
|
||||
} else if !pendingIds.isEmpty && !dbNotificationIds.isEmpty {
|
||||
// Both have data - check if they match
|
||||
if dbNotificationIds != pendingIds {
|
||||
return .coldStart // Mismatch indicates recovery needed
|
||||
} else {
|
||||
return .warmStart // Match indicates warm resume
|
||||
}
|
||||
}
|
||||
|
||||
return .none
|
||||
}
|
||||
```
|
||||
|
||||
**For complete implementation, see**: [Phase 1 directive](./ios-implementation-directive-phase1.md#3-scenario-detection)
|
||||
|
||||
---
|
||||
|
||||
## 5. Missed Notification Detection
|
||||
|
||||
### 5.1 Detection Logic
|
||||
|
||||
**Platform Reference**: [iOS §3.2.1](./alarms/01-platform-capability-reference.md#321-app-code-does-not-run-when-notification-fires) - App code does not run when notification fires
|
||||
|
||||
**iOS Behavior**: When a notification fires, the app code does NOT execute. The notification is displayed, but the app must detect missed notifications on the next app launch.
|
||||
|
||||
**Detection Steps**:
|
||||
|
||||
1. Query database for notifications with `scheduled_time < currentTime`
|
||||
2. Filter for notifications with `delivery_status != 'delivered'`
|
||||
3. Mark as `'missed'` in database
|
||||
4. Record in history table
|
||||
|
||||
**For complete implementation, see**: [Phase 1 directive](./ios-implementation-directive-phase1.md#4-missed-notification-detection)
|
||||
|
||||
---
|
||||
|
||||
## 6. Background Task Registration
|
||||
|
||||
### 6.1 BGTaskScheduler Registration
|
||||
|
||||
**Platform Reference**: [iOS §3.1.3](./alarms/01-platform-capability-reference.md#313-background-tasks-for-prefetching) - Background tasks for prefetching
|
||||
|
||||
**iOS Limitation**: BGTaskScheduler cannot be used for critical scheduling. It's system-controlled and not guaranteed.
|
||||
|
||||
**Use Case**: BGTaskScheduler is used for:
|
||||
- Prefetching content (not critical timing)
|
||||
- Boot recovery (system may defer)
|
||||
- Background maintenance (best effort)
|
||||
|
||||
**Registration Location**: `AppDelegate.swift` or `SceneDelegate.swift`
|
||||
|
||||
**For complete implementation, see**: [Phase 3 directive](./ios-implementation-directive-phase3.md#2-background-task-registration)
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing Strategy
|
||||
|
||||
### 7.1 iOS Testing Tools
|
||||
|
||||
**Simulator Testing**:
|
||||
- `xcrun simctl` - Simulator control
|
||||
- Xcode Instruments - Performance profiling
|
||||
- Console.app - System log viewing
|
||||
|
||||
**Device Testing**:
|
||||
- Xcode Device Console - Real device logs
|
||||
- Settings → Developer → Background App Refresh - Control background execution
|
||||
|
||||
### 7.2 Test Scenarios
|
||||
|
||||
**Phase 1 Tests**:
|
||||
- Cold start recovery
|
||||
- Missed notification detection
|
||||
- Future notification verification
|
||||
|
||||
**Phase 2 Tests**:
|
||||
- App termination detection
|
||||
- Comprehensive recovery
|
||||
- Multiple schedules recovery
|
||||
|
||||
**Phase 3 Tests**:
|
||||
- Background task registration
|
||||
- Boot recovery (simulated)
|
||||
- Background task execution
|
||||
|
||||
**For complete test procedures, see**: [iOS Test Scripts](../test-apps/ios-test-app/test-phase1.sh)
|
||||
|
||||
---
|
||||
|
||||
## 8. Platform-Specific Notes
|
||||
|
||||
### 8.1 Notification Persistence
|
||||
|
||||
**iOS Advantage**: Notifications persist automatically across:
|
||||
- App termination
|
||||
- Device reboot (for calendar/time triggers)
|
||||
|
||||
**App Responsibility**: Must still:
|
||||
- Detect missed notifications on app launch
|
||||
- Reschedule future notifications if needed
|
||||
- Track delivery status in database
|
||||
|
||||
### 8.2 Background Execution Limits
|
||||
|
||||
**iOS Limitation**: Background execution is severely limited:
|
||||
- BGTaskScheduler is system-controlled
|
||||
- Cannot rely on background execution for recovery
|
||||
- Must handle recovery on app launch
|
||||
|
||||
**Workaround**: Use BGTaskScheduler for prefetching only, not for critical scheduling.
|
||||
|
||||
### 8.3 Timing Tolerance
|
||||
|
||||
**iOS Limitation**: Calendar-based notifications have ±180 second tolerance.
|
||||
|
||||
**Impact**: Notifications may fire up to 3 minutes early or late.
|
||||
|
||||
**Mitigation**: Account for tolerance in missed notification detection logic.
|
||||
|
||||
---
|
||||
|
||||
## 9. Next Steps
|
||||
|
||||
1. **Start with Phase 1**: Implement cold start recovery
|
||||
- See [Phase 1 directive](./ios-implementation-directive-phase1.md)
|
||||
- Focus on missed notification detection
|
||||
- Verify future notifications are scheduled
|
||||
|
||||
2. **Proceed to Phase 2**: Add termination detection
|
||||
- See [Phase 2 directive](./ios-implementation-directive-phase2.md)
|
||||
- Implement comprehensive recovery
|
||||
- Handle multiple schedules
|
||||
|
||||
3. **Complete Phase 3**: Background task registration
|
||||
- See [Phase 3 directive](./ios-implementation-directive-phase3.md)
|
||||
- Register BGTaskScheduler handlers
|
||||
- Implement boot recovery
|
||||
|
||||
---
|
||||
|
||||
## 10. References
|
||||
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - iOS OS-level facts
|
||||
- [Plugin Requirements](./alarms/03-plugin-requirements.md) - Requirements this directive implements
|
||||
- [Android Implementation Directive](./android-implementation-directive.md) - Android equivalent for comparison
|
||||
- [iOS Recovery Scenario Mapping](./ios-recovery-scenario-mapping.md) - Detailed scenario mapping
|
||||
- [iOS Core Data Migration Guide](./ios-core-data-migration.md) - Database migration guide
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-08
|
||||
**Next Review**: After Phase 1 implementation
|
||||
|
||||
488
doc/platform/ios/IOS_IMPLEMENTATION_CHECKLIST.md
Normal file
488
doc/platform/ios/IOS_IMPLEMENTATION_CHECKLIST.md
Normal file
@@ -0,0 +1,488 @@
|
||||
# iOS Implementation Checklist
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-12-24
|
||||
**Status**: 🎯 **ACTIVE** - Implementation Tracking
|
||||
**Version**: 1.1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
Complete checklist of iOS code that needs to be implemented for feature parity with Android. This checklist tracks all implementation tasks with checkboxes.
|
||||
|
||||
**Reference**:
|
||||
- [iOS Implementation Directive](./ios-implementation-directive.md) - Implementation guide
|
||||
- [iOS Recovery Scenario Mapping](./ios-recovery-scenario-mapping.md) - Scenario details
|
||||
- [iOS Core Data Migration Guide](./ios-core-data-migration.md) - Database entities
|
||||
- [Legacy Phase 1 Checklist](../../_archive/2025-legacy-doc/IMPLEMENTATION_CHECKLIST_LEGACY.md) - Historical Phase 1 checklist (archived)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Cold Start Recovery (High Priority)
|
||||
|
||||
### 1.1 Create ReactivationManager
|
||||
|
||||
- [x] Create new file: `ios/Plugin/DailyNotificationReactivationManager.swift`
|
||||
- [x] Implement class structure with properties:
|
||||
- [x] `notificationCenter: UNUserNotificationCenter`
|
||||
- [x] `database: DailyNotificationDatabase`
|
||||
- [x] `storage: DailyNotificationStorage`
|
||||
- [x] `scheduler: DailyNotificationScheduler`
|
||||
- [x] `TAG: String = "DNP-REACTIVATION"`
|
||||
- [x] Implement `init(database:storage:scheduler:)` initializer
|
||||
- [x] Implement `performRecovery()` async method
|
||||
- [x] Add timeout protection (2 seconds max)
|
||||
- [x] Add error handling (non-fatal, log only)
|
||||
|
||||
### 1.2 Scenario Detection
|
||||
|
||||
- [x] Create `RecoveryScenario` enum:
|
||||
- [x] `.none` - No recovery needed
|
||||
- [x] `.coldStart` - App launched after termination
|
||||
- [x] `.termination` - App terminated, notifications missing
|
||||
- [x] `.warmStart` - App resumed (optimization)
|
||||
- [x] Implement `detectScenario() async throws -> RecoveryScenario`:
|
||||
- [x] Check if database has notifications (empty → `.none`)
|
||||
- [x] Get pending notifications from `UNUserNotificationCenter`
|
||||
- [x] Compare DB state with notification center state
|
||||
- [x] Return appropriate scenario
|
||||
|
||||
### 1.3 Cold Start Recovery Logic
|
||||
|
||||
- [x] Implement `performColdStartRecovery() async throws -> RecoveryResult`:
|
||||
- [x] Detect missed notifications (scheduled_time < now, not delivered)
|
||||
- [x] Mark missed notifications in database (Phase 1: basic marking, Phase 2: add delivery_status)
|
||||
- [x] Update `last_delivery_attempt` timestamp (Phase 2: add property)
|
||||
- [x] Record in history table (Phase 1: logging only, Phase 2: database recording)
|
||||
- [x] Verify future notifications are scheduled
|
||||
- [x] Reschedule missing future notifications
|
||||
- [x] Return `RecoveryResult` with counts
|
||||
|
||||
### 1.4 Missed Notification Detection
|
||||
|
||||
- [x] Implement `detectMissedNotifications() async throws -> [NotificationContent]`:
|
||||
- [x] Query storage for notifications with `scheduled_time < currentTime`
|
||||
- [x] Filter for missed notifications (Phase 1: time-based only, Phase 2: add delivery_status check)
|
||||
- [x] Return list of missed notifications
|
||||
- [x] Implement `markMissedNotification(_:) async throws`:
|
||||
- [x] Mark notification as missed (Phase 1: basic, Phase 2: add delivery_status property)
|
||||
- [x] Update notification in storage
|
||||
- [x] Record status change (Phase 1: logging, Phase 2: history table)
|
||||
|
||||
### 1.5 Future Notification Verification
|
||||
|
||||
- [x] Implement `verifyFutureNotifications() async throws -> VerificationResult`:
|
||||
- [x] Get all future notifications from storage
|
||||
- [x] Get pending notifications from `UNUserNotificationCenter`
|
||||
- [x] Compare notification IDs
|
||||
- [x] Identify missing notifications
|
||||
- [x] Return verification result
|
||||
- [x] Implement `rescheduleMissingNotification(id:) async throws`:
|
||||
- [x] For each missing notification, reschedule using `DailyNotificationScheduler`
|
||||
- [x] Verify no duplicates created (scheduler handles this)
|
||||
- [x] Log rescheduling activity
|
||||
|
||||
### 1.6 Recovery Result Types
|
||||
|
||||
- [x] Create `RecoveryResult` struct:
|
||||
- [x] `missedCount: Int`
|
||||
- [x] `rescheduledCount: Int`
|
||||
- [x] `verifiedCount: Int`
|
||||
- [x] `errors: Int`
|
||||
- [x] Create `VerificationResult` struct:
|
||||
- [x] `totalSchedules: Int`
|
||||
- [x] `notificationsFound: Int`
|
||||
- [x] `notificationsMissing: Int`
|
||||
- [x] `missingIds: [String]`
|
||||
|
||||
### 1.7 Integration with Plugin
|
||||
|
||||
- [x] Add `reactivationManager` property to `DailyNotificationPlugin`
|
||||
- [x] Initialize `ReactivationManager` in `load()` method
|
||||
- [x] Call `performRecovery()` in `load()` method (async, non-blocking)
|
||||
- [x] Add logging with `DNP-REACTIVATION` tag
|
||||
- [x] Ensure recovery doesn't block app startup (Task-based async execution)
|
||||
|
||||
### 1.8 History Recording
|
||||
|
||||
- [x] Implement `recordRecoveryHistory(_:scenario:)` method:
|
||||
- [x] Record recovery execution (Phase 1: logging with JSON, Phase 2: database table)
|
||||
- [x] Include scenario, counts, outcome
|
||||
- [x] Add diagnostic JSON with details
|
||||
- [x] Implement `recordRecoveryFailure(_:)` method:
|
||||
- [x] Record recovery errors (Phase 1: logging, Phase 2: database table)
|
||||
- [x] Include error message and error type
|
||||
|
||||
### 1.9 Testing
|
||||
|
||||
- [x] Unit tests for scenario detection
|
||||
- [x] Unit tests for missed notification detection
|
||||
- [x] Unit tests for future notification verification
|
||||
- [x] Unit tests for boot detection
|
||||
- [x] Unit tests for recovery result types
|
||||
- [x] Integration test for full recovery flow (DailyNotificationRecoveryIntegrationTests.swift)
|
||||
- [ ] Manual test with test scripts (`test-phase1.sh`)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: App Termination Detection (High Priority)
|
||||
|
||||
### 2.1 Termination Detection Logic
|
||||
|
||||
- [x] Enhance `detectScenario()` to detect termination:
|
||||
- [x] Check if DB has notifications but no pending notifications
|
||||
- [x] Return `.termination` scenario
|
||||
- [x] Implement `handleTerminationRecovery() async throws`:
|
||||
- [x] Detect all missed notifications
|
||||
- [x] Mark all as missed
|
||||
- [x] Reschedule all future notifications
|
||||
- [x] Reschedule all fetch schedules (if applicable)
|
||||
|
||||
### 2.2 Comprehensive Recovery
|
||||
|
||||
- [x] Implement `performFullRecovery() async throws -> RecoveryResult`:
|
||||
- [x] Handle all notifications (missed and future)
|
||||
- [x] Reschedule all missing notifications
|
||||
- [x] Batch operations for efficiency
|
||||
- [x] Return comprehensive result
|
||||
|
||||
### 2.3 Multiple Schedules Recovery
|
||||
|
||||
- [x] Implement recovery for multiple schedules:
|
||||
- [x] Handle multiple notifications (batch processing)
|
||||
- [x] Batch operations for efficiency (single pending request query)
|
||||
- [x] Handle partial failures gracefully (continue on error)
|
||||
- [x] Separate missed vs future notifications for batch processing
|
||||
|
||||
### 2.4 Testing
|
||||
|
||||
- [x] Test termination detection accuracy (testFullRecoveryFlow_Termination in DailyNotificationRecoveryIntegrationTests)
|
||||
- [x] Test full recovery with multiple schedules (testFullRecoveryFlow_Termination tests 3 notifications)
|
||||
- [x] Test partial failure scenarios (testErrorHandling_* tests in DailyNotificationRecoveryIntegrationTests)
|
||||
- [ ] Manual test with test scripts (`test-phase2.sh`)
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Background Task Registration & Boot Recovery (Medium Priority)
|
||||
|
||||
### 3.1 BGTaskScheduler Registration
|
||||
|
||||
- [x] Verify `BGTaskScheduler` registration in `DailyNotificationPlugin.setupBackgroundTasks()`:
|
||||
- [x] Check `fetchTaskIdentifier` registration (already implemented)
|
||||
- [x] Check `notifyTaskIdentifier` registration (already implemented)
|
||||
- [x] Add verification method `verifyBGTaskRegistration()` in ReactivationManager
|
||||
- [x] Implement boot detection:
|
||||
- [x] Check system uptime on app launch
|
||||
- [x] Compare with last launch time (stored in UserDefaults)
|
||||
- [x] Detect if boot occurred recently (< 60 seconds threshold)
|
||||
|
||||
### 3.2 Boot Recovery Logic
|
||||
|
||||
- [x] Implement `performBootRecovery() async throws`:
|
||||
- [x] Detect all missed notifications (past scheduled times)
|
||||
- [x] Mark all as missed
|
||||
- [x] Reschedule all future notifications
|
||||
- [x] Record boot recovery in history
|
||||
|
||||
### 3.3 Background Task Handlers
|
||||
|
||||
- [x] Enhance `handleBackgroundFetch` in `DailyNotificationPlugin.swift`:
|
||||
- [x] Add recovery logic if needed (verification of scheduled notifications)
|
||||
- [x] Schedule next background task (using getNextScheduledNotificationTime)
|
||||
- [x] Handle expiration gracefully (enhanced expiration handler with cleanup)
|
||||
- [x] Enhance `handleBackgroundNotify`:
|
||||
- [x] Add recovery logic if needed (verification of scheduled notifications)
|
||||
- [x] Schedule next background task (helper method added)
|
||||
|
||||
### 3.4 Testing
|
||||
|
||||
- [x] Test BGTaskScheduler registration (verifyBGTaskRegistration method exists, manual verification recommended)
|
||||
- [x] Test boot detection (testDetectBootScenario_* tests in DailyNotificationReactivationManagerTests)
|
||||
- [x] Test boot recovery logic (performBootRecovery tested via integration tests)
|
||||
- [ ] Manual test with test scripts (`test-phase3.sh`)
|
||||
|
||||
---
|
||||
|
||||
## Core Data Entities (High Priority)
|
||||
|
||||
### 4.1 NotificationContent Entity
|
||||
|
||||
- [x] Update `DailyNotificationModel.xcdatamodeld`:
|
||||
- [x] Add `NotificationContent` entity
|
||||
- [x] Add all 23 attributes (id, pluginVersion, timesafariDid, etc.)
|
||||
- [x] Set correct attribute types (String, Date, Int32, Int64, Bool)
|
||||
- [x] Add default values where specified
|
||||
- [x] Mark required vs optional attributes
|
||||
- [x] Add indexes:
|
||||
- [x] `timesafariDid` index
|
||||
- [x] `notificationType` index
|
||||
- [x] `scheduledTime` index
|
||||
- [x] Note: Core Data auto-generates class files with `codeGenerationType="class"`
|
||||
- [x] Implement data conversion helpers (DailyNotificationDataConversions.swift):
|
||||
- [x] `Date` ↔ `Long` (epoch milliseconds) conversion helpers (`dateFromEpochMillis`, `epochMillisFromDate`)
|
||||
- [x] `Int64` ↔ `Long` conversion helpers (`int64FromLong`, `int32FromInt`)
|
||||
|
||||
### 4.2 NotificationDelivery Entity
|
||||
|
||||
- [x] Update `DailyNotificationModel.xcdatamodeld`:
|
||||
- [x] Add `NotificationDelivery` entity
|
||||
- [x] Add all 20 attributes
|
||||
- [x] Set correct attribute types
|
||||
- [x] Add default values
|
||||
- [x] Configure relationship:
|
||||
- [x] Add `notificationContent` relationship (to-one)
|
||||
- [x] Set deletion rule to `Nullify` (Core Data handles cascade via inverse)
|
||||
- [x] Add inverse relationship `deliveries` (to-many) on `NotificationContent`
|
||||
- [x] Add indexes:
|
||||
- [x] `notificationId` index
|
||||
- [x] `deliveryTimestamp` index
|
||||
- [x] Note: Core Data auto-generates class files
|
||||
|
||||
### 4.3 NotificationConfig Entity
|
||||
|
||||
- [x] Update `DailyNotificationModel.xcdatamodeld`:
|
||||
- [x] Add `NotificationConfig` entity
|
||||
- [x] Add all 13 attributes
|
||||
- [x] Set correct attribute types
|
||||
- [x] Add default values
|
||||
- [x] Add indexes:
|
||||
- [x] `configKey` index
|
||||
- [x] `configType` index
|
||||
- [x] `timesafariDid` index
|
||||
- [x] Note: Core Data auto-generates class files
|
||||
|
||||
### 4.4 Data Access Layer
|
||||
|
||||
- [x] Create DAO classes or extensions:
|
||||
- [x] `NotificationContentDAO` or extension methods
|
||||
- [x] `NotificationDeliveryDAO` or extension methods
|
||||
- [x] `NotificationConfigDAO` or extension methods
|
||||
- [x] Implement CRUD operations:
|
||||
- [x] Create/Insert methods
|
||||
- [x] Read/Query methods with predicates
|
||||
- [x] Update methods
|
||||
- [x] Delete methods
|
||||
- [x] Implement query helpers:
|
||||
- [x] Query by timesafariDid
|
||||
- [x] Query by notificationType
|
||||
- [x] Query by scheduledTime range
|
||||
- [x] Query by deliveryStatus
|
||||
|
||||
### 4.5 Persistence Controller Updates
|
||||
|
||||
- [x] Update `PersistenceController` (if exists) or create:
|
||||
- [x] Handle new entities in initialization
|
||||
- [x] Add migration policies if needed
|
||||
- [x] Test database initialization (unit tests verify Core Data stack)
|
||||
- [x] Test Core Data stack:
|
||||
- [x] Entity creation (tested in DAO unit tests)
|
||||
- [x] Relationships (tested in NotificationDeliveryDAOTests)
|
||||
- [x] Cascade delete (tested in NotificationDeliveryDAOTests)
|
||||
- [x] Data conversion (tested in DailyNotificationDataConversionsTests)
|
||||
|
||||
---
|
||||
|
||||
## API Methods (Medium Priority)
|
||||
|
||||
### 5.1 Notification Permission Methods
|
||||
|
||||
- [x] Implement `getNotificationPermissionStatus()`:
|
||||
- [x] Query `UNUserNotificationCenter.current().getNotificationSettings()`
|
||||
- [x] Map to `NotificationPermissionStatus` type
|
||||
- [x] Return authorization status
|
||||
- [x] Implement `requestNotificationPermission()`:
|
||||
- [x] Request authorization via `UNUserNotificationCenter`
|
||||
- [x] Handle user response
|
||||
- [x] Return `{ granted: boolean }`
|
||||
- [x] Implement `openNotificationSettings()`:
|
||||
- [x] Open iOS Settings app to notification settings
|
||||
- [x] Use `UIApplication.shared.open()` with settings URL
|
||||
|
||||
### 5.2 Background Task Methods
|
||||
|
||||
- [x] Implement `getBackgroundTaskStatus()`:
|
||||
- [x] Check BGTaskScheduler registration
|
||||
- [x] Check Background App Refresh status (cannot check programmatically, return null)
|
||||
- [x] Return `BackgroundTaskStatus` object
|
||||
- [x] Implement `openBackgroundAppRefreshSettings()`:
|
||||
- [x] Open iOS Settings app to Background App Refresh
|
||||
- [x] Use `UIApplication.shared.open()` with settings URL
|
||||
|
||||
### 5.3 Pending Notifications Method
|
||||
|
||||
- [x] Implement `getPendingNotifications()`:
|
||||
- [x] Query `UNUserNotificationCenter.current().getPendingNotificationRequests()`
|
||||
- [x] Map to `PendingNotification[]` array
|
||||
- [x] Return count and notification details
|
||||
- [x] Add to `pluginMethods` array in `DailyNotificationPlugin`
|
||||
|
||||
### 5.4 Register Methods in Plugin
|
||||
|
||||
- [x] Add methods to `pluginMethods` array:
|
||||
- [x] `getNotificationPermissionStatus`
|
||||
- [x] `requestNotificationPermission`
|
||||
- [x] `getPendingNotifications`
|
||||
- [x] `getBackgroundTaskStatus`
|
||||
- [x] `openNotificationSettings`
|
||||
- [x] `openBackgroundAppRefreshSettings`
|
||||
|
||||
---
|
||||
|
||||
## Data Type Conversions (High Priority)
|
||||
|
||||
### 6.1 Time Conversions
|
||||
|
||||
- [x] Create helper functions:
|
||||
- [x] `dateFromEpochMillis(_: Int64) -> Date`
|
||||
- [x] `epochMillisFromDate(_: Date) -> Int64`
|
||||
- [x] Use in all Core Data operations:
|
||||
- [x] When reading from database (Long → Date)
|
||||
- [x] When writing to database (Date → Long)
|
||||
|
||||
### 6.2 Numeric Conversions
|
||||
|
||||
- [x] Ensure correct type mappings:
|
||||
- [x] `Int` → `Int32` for small integers
|
||||
- [x] `Long` → `Int64` for large integers
|
||||
- [x] `Boolean` → `Bool` (direct)
|
||||
|
||||
### 6.3 String Conversions
|
||||
|
||||
- [x] Handle optional strings correctly:
|
||||
- [x] `String?` in Swift maps to optional in Core Data
|
||||
- [x] JSON fields stored as `String?`
|
||||
|
||||
---
|
||||
|
||||
## Logging & Observability (Medium Priority)
|
||||
|
||||
### 7.1 Recovery Logging
|
||||
|
||||
- [x] Add comprehensive logging:
|
||||
- [x] `DNP-REACTIVATION: Starting app launch recovery`
|
||||
- [x] `DNP-REACTIVATION: Detected scenario: [scenario]`
|
||||
- [x] `DNP-REACTIVATION: Missed notifications detected: [count]`
|
||||
- [x] `DNP-REACTIVATION: Future notifications verified: [count]`
|
||||
- [x] `DNP-REACTIVATION: Recovery completed: [result]`
|
||||
- [x] Add error logging:
|
||||
- [x] `DNP-REACTIVATION: Recovery failed (non-fatal): [error]`
|
||||
- [x] Include error details and stack trace (NSError domain, code, userInfo)
|
||||
|
||||
### 7.2 Metrics Recording
|
||||
|
||||
- [x] Record recovery metrics in history table:
|
||||
- [x] Recovery execution time (tracked with startTime/endTime)
|
||||
- [x] Missed notification count
|
||||
- [x] Rescheduled notification count
|
||||
- [x] Error count
|
||||
- [x] Add diagnostic JSON to history entries (via HistoryDAO.recordRecovery)
|
||||
|
||||
---
|
||||
|
||||
## Error Handling (High Priority)
|
||||
|
||||
### 8.1 Recovery Error Handling
|
||||
|
||||
- [x] Ensure all recovery methods catch errors:
|
||||
- [x] Database errors (non-fatal) - handled in detectScenario, detectMissedNotifications, verifyFutureNotifications
|
||||
- [x] Notification center errors (non-fatal) - handled in detectScenario, verifyFutureNotifications
|
||||
- [x] Scheduling errors (non-fatal) - handled in rescheduleMissingNotification
|
||||
- [x] Log errors but don't crash app - all errors logged with NSLog, app continues
|
||||
- [x] Return partial results if some operations fail - RecoveryResult includes error count
|
||||
|
||||
### 8.2 Error Types
|
||||
|
||||
- [x] Define iOS-specific error codes:
|
||||
- [x] `NOTIFICATION_PERMISSION_DENIED`
|
||||
- [x] `BACKGROUND_REFRESH_DISABLED`
|
||||
- [x] `PENDING_NOTIFICATION_LIMIT_EXCEEDED`
|
||||
- [x] `BG_TASK_NOT_REGISTERED`
|
||||
- [x] `BG_TASK_EXECUTION_FAILED`
|
||||
- [x] Map to error responses in plugin methods - getNotificationPermissionStatus uses NOTIFICATION_PERMISSION_DENIED
|
||||
|
||||
---
|
||||
|
||||
## Testing (High Priority)
|
||||
|
||||
### 9.1 Unit Tests
|
||||
|
||||
- [x] Test `ReactivationManager` initialization (DailyNotificationReactivationManagerTests)
|
||||
- [x] Test scenario detection logic:
|
||||
- [x] Test `.none` scenario (empty database)
|
||||
- [x] Test `.coldStart` scenario
|
||||
- [x] Test `.termination` scenario
|
||||
- [x] Test `.warmStart` scenario
|
||||
- [x] Test missed notification detection
|
||||
- [x] Test future notification verification
|
||||
- [x] Test recovery result creation
|
||||
- [x] Test data conversions (DailyNotificationDataConversionsTests)
|
||||
- [x] Test NotificationContentDAO (NotificationContentDAOTests)
|
||||
- [x] Test NotificationDeliveryDAO (NotificationDeliveryDAOTests)
|
||||
- [x] Test NotificationConfigDAO (NotificationConfigDAOTests)
|
||||
|
||||
### 9.2 Integration Tests
|
||||
|
||||
- [x] Test full recovery flow:
|
||||
- [x] Schedule notification
|
||||
- [x] Terminate app (simulated by clearing notifications)
|
||||
- [x] Launch app (simulated by calling performRecovery)
|
||||
- [x] Verify recovery executed
|
||||
- [x] Verify notifications rescheduled (DailyNotificationRecoveryIntegrationTests)
|
||||
- [x] Test error handling:
|
||||
- [x] Test database errors (testErrorHandling_DatabaseError)
|
||||
- [x] Test notification center errors (testErrorHandling_NotificationCenterError)
|
||||
- [x] Verify app doesn't crash (all stability tests)
|
||||
|
||||
### 9.3 Manual Testing
|
||||
|
||||
- [ ] Run `test-phase1.sh` script
|
||||
- [ ] Run `test-phase2.sh` script
|
||||
- [ ] Run `test-phase3.sh` script
|
||||
- [ ] Test on physical device (not just simulator)
|
||||
- [ ] Test with Background App Refresh enabled/disabled
|
||||
- [ ] Test with notification permission granted/denied
|
||||
|
||||
---
|
||||
|
||||
## Documentation Updates (Low Priority)
|
||||
|
||||
### 10.1 Code Documentation
|
||||
|
||||
- [x] Add file-level documentation to `DailyNotificationReactivationManager.swift`
|
||||
- [x] Add method-level documentation to all public methods
|
||||
- [x] Add parameter documentation (@param tags)
|
||||
- [x] Add return value documentation (@return tags)
|
||||
- [x] Add error documentation (@throws tags and error handling notes)
|
||||
|
||||
### 10.2 Implementation Status
|
||||
|
||||
- [x] Update `ios/Plugin/README.md` with implementation status
|
||||
- [x] Mark completed features as ✅
|
||||
- [x] Update version numbers (1.1.0)
|
||||
- [x] Update "Last Updated" dates (2025-12-08)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Total Tasks**: ~150+ implementation tasks
|
||||
|
||||
**Priority Breakdown**:
|
||||
- **High Priority**: ~80 tasks (Phase 1, Core Data, API methods, Error handling)
|
||||
- **Medium Priority**: ~50 tasks (Phase 2, Phase 3, Logging)
|
||||
- **Low Priority**: ~20 tasks (Documentation)
|
||||
|
||||
**Estimated Implementation Time**:
|
||||
- Phase 1: 2-3 days
|
||||
- Phase 2: 1-2 days
|
||||
- Phase 3: 1 day
|
||||
- Core Data: 2-3 days
|
||||
- API Methods: 1 day
|
||||
- Testing: 2-3 days
|
||||
- **Total**: ~10-15 days
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.1.0
|
||||
**Last Updated**: 2025-12-24
|
||||
**Next Review**: After manual testing completion
|
||||
|
||||
435
doc/platform/ios/MIGRATION_GUIDE.md
Normal file
435
doc/platform/ios/MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,435 @@
|
||||
# Daily Notification Plugin Migration Guide
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Version**: 2.0.0
|
||||
**Created**: 2025-09-22 09:22:32 UTC
|
||||
**Last Updated**: 2025-09-22 09:22:32 UTC
|
||||
|
||||
## Overview
|
||||
|
||||
This migration guide helps you transition from the basic daily notification plugin to the enhanced version with dual scheduling, callback support, and comprehensive observability.
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
### API Changes
|
||||
|
||||
#### New Methods Added
|
||||
|
||||
- `scheduleContentFetch()` - Schedule content fetching separately
|
||||
- `scheduleUserNotification()` - Schedule user notifications separately
|
||||
- `scheduleDualNotification()` - Schedule both content fetch and notification
|
||||
- `getDualScheduleStatus()` - Get comprehensive status information
|
||||
- `registerCallback()` - Register callback functions
|
||||
- `unregisterCallback()` - Remove callback functions
|
||||
- `getRegisteredCallbacks()` - List registered callbacks
|
||||
|
||||
#### Enhanced Configuration
|
||||
|
||||
- New `DualScheduleConfiguration` interface
|
||||
- Enhanced `NotificationOptions` with callback support
|
||||
- New `ContentFetchConfig` and `UserNotificationConfig` interfaces
|
||||
|
||||
### Platform Requirements
|
||||
|
||||
#### Android
|
||||
|
||||
- **Minimum SDK**: API 21 (Android 5.0)
|
||||
- **Target SDK**: API 34 (Android 14)
|
||||
- **Permissions**: `POST_NOTIFICATIONS`, `SCHEDULE_EXACT_ALARM`
|
||||
- **Dependencies**: Room 2.6.1+, WorkManager 2.9.0+
|
||||
|
||||
#### iOS
|
||||
|
||||
- **Minimum Version**: iOS 13.0
|
||||
- **Background Modes**: Background App Refresh, Background Processing
|
||||
- **Permissions**: Notification permissions required
|
||||
- **Dependencies**: Core Data, BGTaskScheduler
|
||||
|
||||
#### Web
|
||||
|
||||
- **Service Worker**: Required for background functionality
|
||||
- **HTTPS**: Required for Service Worker and push notifications
|
||||
- **Browser Support**: Chrome 40+, Firefox 44+, Safari 11.1+
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### Step 1: Update Dependencies
|
||||
|
||||
```bash
|
||||
npm install @timesafari/daily-notification-plugin@^2.0.0
|
||||
```
|
||||
|
||||
### Step 2: Update Import Statements
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
import { DailyNotification } from '@timesafari/daily-notification-plugin';
|
||||
|
||||
// After
|
||||
import {
|
||||
DailyNotification,
|
||||
DualScheduleConfiguration,
|
||||
ContentFetchConfig,
|
||||
UserNotificationConfig,
|
||||
CallbackEvent
|
||||
} from '@timesafari/daily-notification-plugin';
|
||||
```
|
||||
|
||||
### Step 3: Update Configuration
|
||||
|
||||
#### Basic Migration (Minimal Changes)
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
await DailyNotification.scheduleDailyNotification({
|
||||
title: 'Daily Update',
|
||||
body: 'Your daily content is ready',
|
||||
schedule: '0 9 * * *'
|
||||
});
|
||||
|
||||
// After (backward compatible)
|
||||
await DailyNotification.scheduleDailyNotification({
|
||||
title: 'Daily Update',
|
||||
body: 'Your daily content is ready',
|
||||
schedule: '0 9 * * *'
|
||||
});
|
||||
```
|
||||
|
||||
#### Enhanced Migration (Recommended)
|
||||
|
||||
```typescript
|
||||
// After (enhanced with dual scheduling)
|
||||
const config: DualScheduleConfiguration = {
|
||||
contentFetch: {
|
||||
schedule: '0 8 * * *', // Fetch at 8 AM
|
||||
ttlSeconds: 3600, // 1 hour TTL
|
||||
source: 'api',
|
||||
url: 'https://api.example.com/daily-content'
|
||||
},
|
||||
userNotification: {
|
||||
schedule: '0 9 * * *', // Notify at 9 AM
|
||||
title: 'Daily Update',
|
||||
body: 'Your daily content is ready',
|
||||
actions: [
|
||||
{ id: 'view', title: 'View' },
|
||||
{ id: 'dismiss', title: 'Dismiss' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
await DailyNotification.scheduleDualNotification(config);
|
||||
```
|
||||
|
||||
### Step 4: Add Callback Support
|
||||
|
||||
```typescript
|
||||
// Register callbacks for external integrations
|
||||
await DailyNotification.registerCallback('analytics', {
|
||||
kind: 'http',
|
||||
target: 'https://analytics.example.com/events',
|
||||
headers: {
|
||||
'Authorization': 'Bearer your-token',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
await DailyNotification.registerCallback('database', {
|
||||
kind: 'local',
|
||||
target: 'saveToDatabase'
|
||||
});
|
||||
|
||||
// Local callback function
|
||||
function saveToDatabase(event: CallbackEvent) {
|
||||
console.log('Saving to database:', event);
|
||||
// Your database save logic here
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Update Status Monitoring
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
const status = await DailyNotification.getNotificationStatus();
|
||||
|
||||
// After (enhanced status)
|
||||
const status = await DailyNotification.getDualScheduleStatus();
|
||||
console.log('Next runs:', status.nextRuns);
|
||||
console.log('Cache age:', status.cacheAgeMs);
|
||||
console.log('Circuit breakers:', status.circuitBreakers);
|
||||
console.log('Performance:', status.performance);
|
||||
```
|
||||
|
||||
## Platform-Specific Migration
|
||||
|
||||
### Android Migration
|
||||
|
||||
#### Update AndroidManifest.xml
|
||||
|
||||
```xml
|
||||
<!-- Add new permissions -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!-- Register new receivers -->
|
||||
<receiver android:name="org.timesafari.dailynotification.NotifyReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<receiver android:name="org.timesafari.dailynotification.BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
```
|
||||
|
||||
#### Update build.gradle
|
||||
|
||||
```gradle
|
||||
dependencies {
|
||||
implementation "androidx.room:room-runtime:2.6.1"
|
||||
implementation "androidx.work:work-runtime-ktx:2.9.0"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
||||
annotationProcessor "androidx.room:room-compiler:2.6.1"
|
||||
}
|
||||
```
|
||||
|
||||
### iOS Migration
|
||||
|
||||
#### Update Info.plist
|
||||
|
||||
```xml
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>background-app-refresh</string>
|
||||
<string>background-processing</string>
|
||||
</array>
|
||||
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>org.timesafari.dailynotification.content-fetch</string>
|
||||
<string>org.timesafari.dailynotification.notification-delivery</string>
|
||||
</array>
|
||||
```
|
||||
|
||||
#### Update Capabilities
|
||||
|
||||
1. Enable "Background Modes" capability
|
||||
2. Enable "Background App Refresh"
|
||||
3. Enable "Background Processing"
|
||||
|
||||
### Web Migration
|
||||
|
||||
#### Service Worker Registration
|
||||
|
||||
```typescript
|
||||
// Register Service Worker
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(registration => {
|
||||
console.log('Service Worker registered:', registration);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Service Worker registration failed:', error);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### Push Notification Setup
|
||||
|
||||
```typescript
|
||||
// Request notification permission
|
||||
const permission = await Notification.requestPermission();
|
||||
|
||||
if (permission === 'granted') {
|
||||
// Subscribe to push notifications
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: 'your-vapid-public-key'
|
||||
});
|
||||
|
||||
// Send subscription to your server
|
||||
await fetch('/api/push-subscription', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(subscription)
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Migration
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```typescript
|
||||
import { DailyNotification } from '@timesafari/daily-notification-plugin';
|
||||
|
||||
describe('Migration Tests', () => {
|
||||
test('backward compatibility', async () => {
|
||||
// Test that old API still works
|
||||
await DailyNotification.scheduleDailyNotification({
|
||||
title: 'Test',
|
||||
body: 'Test body',
|
||||
schedule: '0 9 * * *'
|
||||
});
|
||||
});
|
||||
|
||||
test('new dual scheduling', async () => {
|
||||
const config = {
|
||||
contentFetch: { schedule: '0 8 * * *', ttlSeconds: 3600 },
|
||||
userNotification: { schedule: '0 9 * * *', title: 'Test' }
|
||||
};
|
||||
|
||||
await DailyNotification.scheduleDualNotification(config);
|
||||
const status = await DailyNotification.getDualScheduleStatus();
|
||||
expect(status.nextRuns).toBeDefined();
|
||||
});
|
||||
|
||||
test('callback registration', async () => {
|
||||
await DailyNotification.registerCallback('test', {
|
||||
kind: 'local',
|
||||
target: 'testCallback'
|
||||
});
|
||||
|
||||
const callbacks = await DailyNotification.getRegisteredCallbacks();
|
||||
expect(callbacks).toContain('test');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```typescript
|
||||
describe('Integration Tests', () => {
|
||||
test('end-to-end dual scheduling', async () => {
|
||||
// Schedule content fetch
|
||||
await DailyNotification.scheduleContentFetch({
|
||||
schedule: '0 8 * * *',
|
||||
ttlSeconds: 3600,
|
||||
source: 'api',
|
||||
url: 'https://api.example.com/content'
|
||||
});
|
||||
|
||||
// Schedule notification
|
||||
await DailyNotification.scheduleUserNotification({
|
||||
schedule: '0 9 * * *',
|
||||
title: 'Daily Update',
|
||||
body: 'Content ready'
|
||||
});
|
||||
|
||||
// Verify status
|
||||
const status = await DailyNotification.getDualScheduleStatus();
|
||||
expect(status.nextRuns.length).toBe(2);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Android
|
||||
|
||||
- **Permission Denied**: Ensure all required permissions are declared
|
||||
- **WorkManager Not Running**: Check battery optimization settings
|
||||
- **Database Errors**: Verify Room database schema migration
|
||||
|
||||
#### iOS
|
||||
|
||||
- **Background Tasks Not Running**: Check Background App Refresh settings
|
||||
- **Core Data Errors**: Verify Core Data model compatibility
|
||||
- **Notification Permissions**: Request notification permissions
|
||||
|
||||
#### Web
|
||||
|
||||
- **Service Worker Not Registering**: Ensure HTTPS and proper file paths
|
||||
- **Push Notifications Not Working**: Verify VAPID keys and server setup
|
||||
- **IndexedDB Errors**: Check browser compatibility and storage quotas
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```typescript
|
||||
// Get comprehensive status
|
||||
const status = await DailyNotification.getDualScheduleStatus();
|
||||
console.log('Status:', status);
|
||||
|
||||
// Check registered callbacks
|
||||
const callbacks = await DailyNotification.getRegisteredCallbacks();
|
||||
console.log('Callbacks:', callbacks);
|
||||
|
||||
// Test callback firing
|
||||
await DailyNotification.registerCallback('debug', {
|
||||
kind: 'local',
|
||||
target: 'debugCallback'
|
||||
});
|
||||
|
||||
function debugCallback(event: CallbackEvent) {
|
||||
console.log('Debug callback fired:', event);
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Memory Usage
|
||||
|
||||
- **Android**: Room database with connection pooling
|
||||
- **iOS**: Core Data with lightweight contexts
|
||||
- **Web**: IndexedDB with efficient indexing
|
||||
|
||||
### Battery Optimization
|
||||
|
||||
- **Android**: WorkManager with battery-aware constraints
|
||||
- **iOS**: BGTaskScheduler with system-managed execution
|
||||
- **Web**: Service Worker with efficient background sync
|
||||
|
||||
### Network Usage
|
||||
|
||||
- **Circuit Breaker**: Prevents excessive retry attempts
|
||||
- **TTL-at-Fire**: Reduces unnecessary network calls
|
||||
- **Exponential Backoff**: Intelligent retry scheduling
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Permissions
|
||||
|
||||
- **Minimal Permissions**: Only request necessary permissions
|
||||
- **Runtime Checks**: Verify permissions before operations
|
||||
- **Graceful Degradation**: Handle permission denials gracefully
|
||||
|
||||
### Data Protection
|
||||
|
||||
- **Local Storage**: Encrypted local storage on all platforms
|
||||
- **Network Security**: HTTPS-only for all network operations
|
||||
- **Callback Security**: Validate callback URLs and headers
|
||||
|
||||
### Privacy
|
||||
|
||||
- **No Personal Data**: Plugin doesn't collect personal information
|
||||
- **Local Processing**: All processing happens locally
|
||||
- **User Control**: Users can disable notifications and callbacks
|
||||
|
||||
## Support
|
||||
|
||||
### Documentation
|
||||
|
||||
- **API Reference**: Complete TypeScript definitions
|
||||
- **Examples**: Comprehensive usage examples
|
||||
- **Troubleshooting**: Common issues and solutions
|
||||
|
||||
### Community
|
||||
|
||||
- **GitHub Issues**: Report bugs and request features
|
||||
- **Discussions**: Ask questions and share solutions
|
||||
- **Contributing**: Submit pull requests and improvements
|
||||
|
||||
### Enterprise Support
|
||||
|
||||
- **Custom Implementations**: Tailored solutions for enterprise needs
|
||||
- **Integration Support**: Help with complex integrations
|
||||
- **Performance Optimization**: Custom performance tuning
|
||||
|
||||
---
|
||||
|
||||
**Next Steps**: After migration, explore the [Enterprise Callback Examples](./enterprise-callback-examples.md) for advanced integration patterns.
|
||||
74
doc/platform/ios/PREFETCH_GLOSSARY.md
Normal file
74
doc/platform/ios/PREFETCH_GLOSSARY.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# iOS Prefetch Glossary
|
||||
|
||||
**Purpose:** Shared terminology definitions for iOS prefetch testing and implementation
|
||||
|
||||
**Last Updated:** 2025-11-15
|
||||
**Status:** 🎯 **ACTIVE** - Reference glossary for iOS prefetch documentation
|
||||
|
||||
---
|
||||
|
||||
## Core Terms
|
||||
|
||||
**BGTaskScheduler** – iOS framework for scheduling background tasks (BGAppRefreshTask / BGProcessingTask). Provides heuristic-based background execution, not exact timing guarantees.
|
||||
|
||||
**BGAppRefreshTask** – Specific BGTaskScheduler task type for background app refresh. Used for prefetch operations that need to run periodically.
|
||||
|
||||
**UNUserNotificationCenter** – iOS notification framework for scheduling and delivering user notifications. Handles permission requests and notification delivery.
|
||||
|
||||
**T-Lead** – The lead time between prefetch and notification fire, e.g., 5 minutes. Prefetch is scheduled at `notificationTime - T-Lead`.
|
||||
|
||||
**earliestBeginDate** – The earliest time iOS may execute a BGTask. This is a hint, not a guarantee; iOS may run the task later based on heuristics.
|
||||
|
||||
**UTC** – Coordinated Universal Time. All internal timestamps are stored in UTC to avoid DST and timezone issues.
|
||||
|
||||
---
|
||||
|
||||
## Behavior Classification
|
||||
|
||||
**Bucket A/B/C** – Deterministic vs heuristic classification used in Behavior Classification:
|
||||
|
||||
- **Bucket A (Deterministic):** Test in Simulator and Device - Logic correctness
|
||||
- **Bucket B (Partially Deterministic):** Test flow in Simulator, timing on Device
|
||||
- **Bucket C (Heuristic):** Test on Real Device only - Timing and reliability
|
||||
|
||||
**Deterministic** – Behavior that produces the same results given the same inputs, regardless of when or where it runs. Can be fully tested in simulator.
|
||||
|
||||
**Heuristic** – Behavior controlled by iOS system heuristics (user patterns, battery, network, etc.). Timing is not guaranteed and must be tested on real devices.
|
||||
|
||||
---
|
||||
|
||||
## Testing Terms
|
||||
|
||||
**Happy Path** – The expected successful execution flow: Schedule → BGTask → Fetch → Cache → Notification Delivery.
|
||||
|
||||
**Negative Path** – Failure scenarios that test error handling: Network failures, permission denials, expired tokens, etc.
|
||||
|
||||
**Telemetry** – Structured metrics and counters emitted by the plugin for observability (e.g., `dnp_prefetch_scheduled_total`).
|
||||
|
||||
**Log Sequence** – The ordered sequence of log messages that indicate successful execution of a prefetch cycle.
|
||||
|
||||
---
|
||||
|
||||
## Platform Terms
|
||||
|
||||
**Simulator** – iOS Simulator for testing logic correctness. BGTask execution can be manually triggered.
|
||||
|
||||
**Real Device** – Physical iOS device for testing timing and reliability. BGTask execution is controlled by iOS heuristics.
|
||||
|
||||
**Background App Refresh** – iOS system setting that controls whether apps can perform background tasks. Must be enabled for BGTask execution.
|
||||
|
||||
**Low Power Mode** – iOS system mode that may delay or disable background tasks to conserve battery.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **Testing Guide:** `doc/test-app-ios/IOS_PREFETCH_TESTING.md`
|
||||
- **Test App Requirements:** `doc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.md`
|
||||
- **Main Directive:** `doc/directives/0003-iOS-Android-Parity-Directive.md`
|
||||
|
||||
---
|
||||
|
||||
**Status:** 🎯 **READY FOR USE**
|
||||
**Maintainer:** Matthew Raymer
|
||||
|
||||
423
doc/platform/ios/RECOVERY_SCENARIO_MAPPING.md
Normal file
423
doc/platform/ios/RECOVERY_SCENARIO_MAPPING.md
Normal file
@@ -0,0 +1,423 @@
|
||||
# iOS Recovery Scenario Mapping: Android → iOS Equivalents
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-12-08
|
||||
**Status**: 🎯 **ACTIVE** - Recovery Scenario Mapping Reference
|
||||
**Version**: 1.0.0
|
||||
**Last Synced With Plugin Version**: v1.1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
This document maps Android recovery scenarios to their iOS equivalents, providing a clear translation guide for implementing iOS recovery logic based on Android patterns.
|
||||
|
||||
**Reference**:
|
||||
- [Android Implementation Directive](./android-implementation-directive.md) - Android scenarios
|
||||
- [iOS Implementation Directive](./ios-implementation-directive.md) - iOS scenarios
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - OS-level facts
|
||||
|
||||
---
|
||||
|
||||
## 1. Scenario Mapping Overview
|
||||
|
||||
### 1.1 Direct Mappings
|
||||
|
||||
| Android Scenario | iOS Equivalent | Detection Method | Recovery Action |
|
||||
| ---------------- | -------------- | ---------------- | --------------- |
|
||||
| `COLD_START` | App Launch After Termination | Compare UNUserNotificationCenter vs DB | Detect missed, verify future |
|
||||
| `FORCE_STOP` | App Terminated by System | DB has schedules, no notifications | Full recovery of all schedules |
|
||||
| `BOOT` | Device Reboot | BGTaskScheduler registration | Reschedule all notifications |
|
||||
| `WARM_START` | App Resume (Foreground) | Notifications match DB state | No recovery needed (optimization) |
|
||||
| `NONE` | First Launch / No Recovery | Empty database | No action needed |
|
||||
|
||||
### 1.2 Key Differences
|
||||
|
||||
**iOS Advantages**:
|
||||
- ✅ Notifications persist across termination (OS-guaranteed)
|
||||
- ✅ Notifications persist across reboot (OS-guaranteed)
|
||||
- ❌ No user-facing "force stop" equivalent
|
||||
|
||||
**iOS Challenges**:
|
||||
- ❌ App code does NOT run when notification fires
|
||||
- ❌ Must detect missed notifications on app launch
|
||||
- ❌ Background execution severely limited
|
||||
|
||||
---
|
||||
|
||||
## 2. Detailed Scenario Mappings
|
||||
|
||||
### 2.1 COLD_START → App Launch After Termination
|
||||
|
||||
**Android Definition**:
|
||||
- Process killed, alarms may or may not exist
|
||||
- Database still populated
|
||||
- Alarms may have been cleared by OS
|
||||
|
||||
**iOS Equivalent**:
|
||||
- App terminated by system or user
|
||||
- Notifications may still exist (OS-guaranteed persistence)
|
||||
- Database still populated
|
||||
- Need to verify notification state matches database
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
**Android**:
|
||||
```kotlin
|
||||
// Check if alarms exist in AlarmManager
|
||||
val alarmsExist = alarmManager.hasAlarm(pendingIntent)
|
||||
if (alarmsExist && dbHasSchedules) {
|
||||
return COLD_START
|
||||
}
|
||||
```
|
||||
|
||||
**iOS**:
|
||||
```swift
|
||||
// Check if notifications exist in UNUserNotificationCenter
|
||||
let pendingNotifications = try await notificationCenter.pendingNotificationRequests()
|
||||
let dbSchedules = try database.getEnabledSchedules()
|
||||
|
||||
if !pendingNotifications.isEmpty && !dbSchedules.isEmpty {
|
||||
// Compare notification IDs with DB state
|
||||
let dbIds = Set(dbSchedules.flatMap { $0.getScheduledNotificationIds() })
|
||||
let pendingIds = Set(pendingNotifications.map { $0.identifier })
|
||||
|
||||
if dbIds != pendingIds {
|
||||
return .coldStart // Mismatch indicates recovery needed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Recovery Actions**:
|
||||
1. Detect missed notifications (scheduled_time < now, not delivered)
|
||||
2. Mark missed notifications in database
|
||||
3. Verify future notifications are scheduled
|
||||
4. Reschedule missing future notifications
|
||||
|
||||
**Platform Reference**: [iOS §3.1.1](./alarms/01-platform-capability-reference.md#311-notifications-survive-app-termination)
|
||||
|
||||
---
|
||||
|
||||
### 2.2 FORCE_STOP → App Terminated by System
|
||||
|
||||
**Android Definition**:
|
||||
- User force-stopped app via Settings
|
||||
- All alarms cleared
|
||||
- Database still populated
|
||||
- Boot receiver blocked until user launches app
|
||||
|
||||
**iOS Equivalent**:
|
||||
- App terminated by system (low memory, etc.)
|
||||
- Notifications may be missing (system cleared them)
|
||||
- Database still populated
|
||||
- No user-facing force stop equivalent
|
||||
|
||||
**Key Difference**: iOS doesn't have a user-facing "force stop" option. System termination is the closest equivalent.
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
**Android**:
|
||||
```kotlin
|
||||
// Check if alarms exist
|
||||
val alarmsExist = alarmManager.hasAlarm(pendingIntent)
|
||||
if (!alarmsExist && dbHasSchedules && !isBootRecent) {
|
||||
return FORCE_STOP
|
||||
}
|
||||
```
|
||||
|
||||
**iOS**:
|
||||
```swift
|
||||
// Check if notifications exist
|
||||
let pendingNotifications = try await notificationCenter.pendingNotificationRequests()
|
||||
let dbSchedules = try database.getEnabledSchedules()
|
||||
|
||||
if pendingNotifications.isEmpty && !dbSchedules.isEmpty {
|
||||
// DB has schedules but no notifications scheduled
|
||||
return .termination
|
||||
}
|
||||
```
|
||||
|
||||
**Recovery Actions**:
|
||||
1. Detect all missed notifications
|
||||
2. Mark all missed notifications in database
|
||||
3. Reschedule all future notifications
|
||||
4. Reschedule all fetch schedules (if applicable)
|
||||
|
||||
**Platform Reference**: [iOS §3.2.1](./alarms/01-platform-capability-reference.md#321-app-code-does-not-run-when-notification-fires)
|
||||
|
||||
---
|
||||
|
||||
### 2.3 BOOT → Device Reboot
|
||||
|
||||
**Android Definition**:
|
||||
- Device rebooted
|
||||
- All alarms wiped (OS behavior)
|
||||
- Database still populated
|
||||
- Boot receiver executes after boot completes
|
||||
|
||||
**iOS Equivalent**:
|
||||
- Device rebooted
|
||||
- Notifications persist automatically (OS-guaranteed)
|
||||
- Database still populated
|
||||
- BGTaskScheduler may execute (system-controlled)
|
||||
|
||||
**Key Difference**: iOS automatically persists notifications across reboot. Android requires manual rescheduling.
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
**Android**:
|
||||
```kotlin
|
||||
// Check boot flag (set by BootReceiver)
|
||||
val bootFlag = sharedPreferences.getLong("last_boot_time", 0)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (bootFlag > 0 && (currentTime - bootFlag) < 60000) {
|
||||
return BOOT
|
||||
}
|
||||
```
|
||||
|
||||
**iOS**:
|
||||
```swift
|
||||
// BGTaskScheduler registration handles boot
|
||||
// Check if this is a boot-triggered background task
|
||||
if isBootBackgroundTask {
|
||||
return .boot
|
||||
}
|
||||
|
||||
// Or detect on app launch after reboot
|
||||
let lastLaunchTime = UserDefaults.standard.double(forKey: "last_launch_time")
|
||||
let bootTime = ProcessInfo.processInfo.systemUptime
|
||||
if lastLaunchTime > 0 && bootTime < 60 {
|
||||
return .boot
|
||||
}
|
||||
```
|
||||
|
||||
**Recovery Actions**:
|
||||
1. Verify notifications still exist (iOS usually handles this)
|
||||
2. Detect any missed notifications during reboot window
|
||||
3. Reschedule any missing notifications
|
||||
4. Update next run times for repeating schedules
|
||||
|
||||
**Platform Reference**: [iOS §3.1.2](./alarms/01-platform-capability-reference.md#312-notifications-persist-across-device-reboot)
|
||||
|
||||
---
|
||||
|
||||
### 2.4 WARM_START → App Resume (Foreground)
|
||||
|
||||
**Android Definition**:
|
||||
- App resumed from background
|
||||
- Alarms still exist
|
||||
- Database matches alarm state
|
||||
- No recovery needed (optimization)
|
||||
|
||||
**iOS Equivalent**:
|
||||
- App resumed from background
|
||||
- Notifications still exist
|
||||
- Database matches notification state
|
||||
- No recovery needed (optimization)
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
**Android**:
|
||||
```kotlin
|
||||
// Check if alarms exist and match DB
|
||||
val alarmsExist = alarmManager.hasAlarm(pendingIntent)
|
||||
if (alarmsExist && dbMatchesAlarms) {
|
||||
return WARM_START
|
||||
}
|
||||
```
|
||||
|
||||
**iOS**:
|
||||
```swift
|
||||
// Check if notifications exist and match DB
|
||||
let pendingNotifications = try await notificationCenter.pendingNotificationRequests()
|
||||
let dbSchedules = try database.getEnabledSchedules()
|
||||
|
||||
let dbIds = Set(dbSchedules.flatMap { $0.getScheduledNotificationIds() })
|
||||
let pendingIds = Set(pendingNotifications.map { $0.identifier })
|
||||
|
||||
if dbIds == pendingIds {
|
||||
return .warmStart // Match indicates warm resume
|
||||
}
|
||||
```
|
||||
|
||||
**Recovery Actions**:
|
||||
- None (optimization only)
|
||||
- May perform lightweight verification
|
||||
- May update metrics
|
||||
|
||||
---
|
||||
|
||||
### 2.5 NONE → First Launch / No Recovery
|
||||
|
||||
**Android Definition**:
|
||||
- First app launch
|
||||
- Empty database
|
||||
- No schedules configured
|
||||
- No recovery needed
|
||||
|
||||
**iOS Equivalent**:
|
||||
- First app launch
|
||||
- Empty database
|
||||
- No schedules configured
|
||||
- No recovery needed
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
**Android**:
|
||||
```kotlin
|
||||
// Check if database is empty
|
||||
val schedules = database.scheduleDao().getEnabled()
|
||||
if (schedules.isEmpty()) {
|
||||
return NONE
|
||||
}
|
||||
```
|
||||
|
||||
**iOS**:
|
||||
```swift
|
||||
// Check if database is empty
|
||||
let schedules = try database.getEnabledSchedules()
|
||||
if schedules.isEmpty {
|
||||
return .none
|
||||
}
|
||||
```
|
||||
|
||||
**Recovery Actions**:
|
||||
- None
|
||||
|
||||
---
|
||||
|
||||
## 3. Recovery Action Mapping
|
||||
|
||||
### 3.1 Missed Notification Detection
|
||||
|
||||
**Android**:
|
||||
- Query AlarmManager for past alarms
|
||||
- Check database for undelivered notifications
|
||||
- Mark as missed in database
|
||||
|
||||
**iOS**:
|
||||
- Query database for past scheduled notifications
|
||||
- Check delivery status
|
||||
- Mark as missed in database
|
||||
|
||||
**Key Difference**: iOS cannot query past notifications from UNUserNotificationCenter. Must rely on database state.
|
||||
|
||||
### 3.2 Future Notification Verification
|
||||
|
||||
**Android**:
|
||||
- Query AlarmManager for future alarms
|
||||
- Compare with database schedules
|
||||
- Reschedule missing alarms
|
||||
|
||||
**iOS**:
|
||||
- Query UNUserNotificationCenter for pending notifications
|
||||
- Compare with database schedules
|
||||
- Reschedule missing notifications
|
||||
|
||||
**Key Difference**: iOS uses UNUserNotificationCenter instead of AlarmManager.
|
||||
|
||||
### 3.3 Full Recovery
|
||||
|
||||
**Android**:
|
||||
- Reschedule all notify schedules
|
||||
- Reschedule all fetch schedules (WorkManager)
|
||||
- Mark past notifications as missed
|
||||
|
||||
**iOS**:
|
||||
- Reschedule all notify schedules
|
||||
- Reschedule all fetch schedules (BGTaskScheduler)
|
||||
- Mark past notifications as missed
|
||||
|
||||
**Key Difference**: iOS uses BGTaskScheduler instead of WorkManager.
|
||||
|
||||
---
|
||||
|
||||
## 4. Implementation Checklist
|
||||
|
||||
### 4.1 Phase 1: Cold Start Recovery
|
||||
|
||||
- [ ] Implement scenario detection (cold start)
|
||||
- [ ] Implement missed notification detection
|
||||
- [ ] Implement future notification verification
|
||||
- [ ] Test cold start recovery
|
||||
|
||||
### 4.2 Phase 2: Termination Detection
|
||||
|
||||
- [ ] Implement termination detection
|
||||
- [ ] Implement full recovery logic
|
||||
- [ ] Test termination recovery
|
||||
|
||||
### 4.3 Phase 3: Boot Recovery
|
||||
|
||||
- [ ] Implement BGTaskScheduler registration
|
||||
- [ ] Implement boot detection
|
||||
- [ ] Test boot recovery
|
||||
|
||||
---
|
||||
|
||||
## 5. Platform-Specific Notes
|
||||
|
||||
### 5.1 iOS Advantages
|
||||
|
||||
1. **Notification Persistence**: iOS automatically persists notifications across termination and reboot
|
||||
2. **No Force Stop**: iOS doesn't have user-facing force stop, reducing complexity
|
||||
3. **Simplified Recovery**: Less recovery needed due to OS persistence
|
||||
|
||||
### 5.2 iOS Challenges
|
||||
|
||||
1. **No Code Execution on Fire**: App code doesn't run when notification fires
|
||||
2. **Background Limits**: Severely limited background execution
|
||||
3. **Timing Tolerance**: ±180 second tolerance for calendar triggers
|
||||
|
||||
### 5.3 Android Advantages
|
||||
|
||||
1. **Code Execution on Fire**: PendingIntent can execute code when alarm fires
|
||||
2. **WorkManager**: More reliable background execution
|
||||
3. **Exact Timing**: Can achieve exact timing with permission
|
||||
|
||||
### 5.4 Android Challenges
|
||||
|
||||
1. **No Persistence**: Alarms don't persist across reboot
|
||||
2. **Force Stop**: Hard kill that cannot be bypassed
|
||||
3. **Boot Recovery**: Must implement boot receiver
|
||||
|
||||
---
|
||||
|
||||
## 6. Testing Strategy
|
||||
|
||||
### 6.1 Scenario Testing
|
||||
|
||||
**Cold Start**:
|
||||
1. Terminate app (swipe away)
|
||||
2. Wait for notification time to pass
|
||||
3. Launch app
|
||||
4. Verify missed notification detection
|
||||
5. Verify future notifications rescheduled
|
||||
|
||||
**Termination**:
|
||||
1. Schedule notifications
|
||||
2. Terminate app
|
||||
3. Clear notifications (simulate system clearing)
|
||||
4. Launch app
|
||||
5. Verify full recovery
|
||||
|
||||
**Boot**:
|
||||
1. Schedule notifications
|
||||
2. Reboot device (or simulate)
|
||||
3. Launch app
|
||||
4. Verify notifications still exist
|
||||
5. Verify any missed notifications detected
|
||||
|
||||
---
|
||||
|
||||
## 7. References
|
||||
|
||||
- [Android Implementation Directive](./android-implementation-directive.md) - Android scenarios
|
||||
- [iOS Implementation Directive](./ios-implementation-directive.md) - iOS scenarios
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - OS-level facts
|
||||
- [Plugin Requirements](./alarms/03-plugin-requirements.md) - Requirements
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-08
|
||||
**Next Review**: After Phase 1 implementation
|
||||
|
||||
649
doc/platform/ios/ROLLOVER_EDGE_CASES.md
Normal file
649
doc/platform/ios/ROLLOVER_EDGE_CASES.md
Normal file
@@ -0,0 +1,649 @@
|
||||
# iOS Rollover Implementation — Edge Case Handling Plan
|
||||
|
||||
**Status**: Planning Phase
|
||||
**Priority**: Reliability-First
|
||||
**Author**: AI Assistant
|
||||
**Date**: 2025-01-27
|
||||
|
||||
## Objective
|
||||
|
||||
Implement Android-like automatic rollover for iOS notifications with comprehensive edge case handling to ensure reliability across all scenarios, including time changes, timezone changes, DST transitions, and race conditions.
|
||||
|
||||
---
|
||||
|
||||
## Edge Case Categories
|
||||
|
||||
### 1. **Time Changes**
|
||||
- Manual clock adjustments (user changes device time)
|
||||
- System clock corrections (NTP sync)
|
||||
- Clock drift corrections
|
||||
- Time jumps (forward/backward)
|
||||
|
||||
### 2. **Timezone Changes**
|
||||
- User changes device timezone
|
||||
- Automatic timezone detection changes
|
||||
- Travel across timezones
|
||||
- Timezone database updates
|
||||
|
||||
### 3. **DST Transitions**
|
||||
- Spring forward (lose 1 hour)
|
||||
- Fall back (gain 1 hour)
|
||||
- DST rule changes
|
||||
- Regions that don't observe DST
|
||||
|
||||
### 4. **Race Conditions**
|
||||
- Multiple rollover attempts for same notification
|
||||
- Concurrent scheduling operations
|
||||
- App state transitions during rollover
|
||||
- Background task conflicts
|
||||
|
||||
### 5. **System Events**
|
||||
- Device reboots
|
||||
- App termination
|
||||
- Low memory conditions
|
||||
- Background execution limits
|
||||
|
||||
### 6. **Notification System Edge Cases**
|
||||
- Notification limit reached (64 pending)
|
||||
- Notification delivery failures
|
||||
- System notification queue issues
|
||||
- Permission changes
|
||||
|
||||
---
|
||||
|
||||
## Detection Mechanisms
|
||||
|
||||
### A. Time Change Detection
|
||||
|
||||
**iOS Limitation**: iOS doesn't provide direct time change notifications like Android's `ACTION_TIME_CHANGED` broadcast.
|
||||
|
||||
**Solution**: Multi-layered detection:
|
||||
|
||||
1. **App Launch Detection**
|
||||
- Store last known system time on app exit
|
||||
- Compare on app launch
|
||||
- Detect significant time jumps (>5 minutes)
|
||||
|
||||
2. **Background Task Detection**
|
||||
- Store timestamp when scheduling notification
|
||||
- Compare with current time when background task runs
|
||||
- Detect time discrepancies
|
||||
|
||||
3. **Notification Delivery Detection**
|
||||
- Compare scheduled time with actual delivery time
|
||||
- Flag if delivery time is significantly different
|
||||
|
||||
4. **Periodic Validation**
|
||||
- Background task validates scheduled notifications
|
||||
- Checks if notification times are still valid
|
||||
- Adjusts if time change detected
|
||||
|
||||
### B. Timezone Change Detection
|
||||
|
||||
**iOS Limitation**: No direct timezone change notification.
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. **Store Timezone on Schedule**
|
||||
- Save timezone identifier when scheduling
|
||||
- Store as part of notification metadata
|
||||
|
||||
2. **Compare on Access**
|
||||
- Check current timezone vs stored timezone
|
||||
- Detect changes on app launch, background tasks, rollover
|
||||
|
||||
3. **Recalculate on Change**
|
||||
- If timezone changed, recalculate all scheduled times
|
||||
- Maintain same local time (e.g., 9:00 AM stays 9:00 AM)
|
||||
|
||||
### C. DST Transition Detection
|
||||
|
||||
**Solution**: Use Calendar API for DST-aware calculations:
|
||||
|
||||
1. **Calendar-Based Calculation**
|
||||
- Use `Calendar.date(byAdding: .hour, value: 24, to:)`
|
||||
- Automatically handles DST transitions
|
||||
- No manual DST detection needed
|
||||
|
||||
2. **Validation After Calculation**
|
||||
- Verify calculated time is exactly 24 hours later in local time
|
||||
- Log DST transitions for debugging
|
||||
- Handle edge cases (e.g., 2:00 AM → 3:00 AM spring forward)
|
||||
|
||||
### D. Duplicate Prevention
|
||||
|
||||
**Solution**: Multi-level idempotence checks:
|
||||
|
||||
1. **Database-Level Check**
|
||||
- Store rollover state per notification ID
|
||||
- Track last processed rollover time
|
||||
- Prevent duplicate rollover attempts
|
||||
|
||||
2. **Storage-Level Check**
|
||||
- Check for existing notifications at same scheduled time
|
||||
- Use tolerance window (1 minute) for DST shifts
|
||||
- Compare notification IDs and scheduled times
|
||||
|
||||
3. **System-Level Check**
|
||||
- Query `UNUserNotificationCenter` for pending notifications
|
||||
- Check if notification already scheduled
|
||||
- Cancel and reschedule if needed
|
||||
|
||||
4. **Request-Level Check**
|
||||
- Use unique notification IDs
|
||||
- Include timestamp in ID generation
|
||||
- Prevent ID collisions
|
||||
|
||||
---
|
||||
|
||||
## Handling Strategies
|
||||
|
||||
### Strategy 1: Time Change Handling
|
||||
|
||||
**When Detected**:
|
||||
1. **Validate All Scheduled Notifications**
|
||||
- Check if scheduled times are still valid
|
||||
- Recalculate if time change was significant
|
||||
- Cancel invalid notifications
|
||||
|
||||
2. **Recalculate Rollover Times**
|
||||
- If time changed, recalculate next notification time
|
||||
- Use DST-safe calculation
|
||||
- Maintain same local time (e.g., 9:00 AM)
|
||||
|
||||
3. **Reschedule Affected Notifications**
|
||||
- Cancel old notifications
|
||||
- Schedule with corrected times
|
||||
- Update storage with new times
|
||||
|
||||
4. **Log Time Change Event**
|
||||
- Record time change in history
|
||||
- Log old time, new time, delta
|
||||
- Track which notifications were affected
|
||||
|
||||
**Implementation**:
|
||||
```swift
|
||||
func handleTimeChange(
|
||||
lastKnownTime: Int64,
|
||||
currentTime: Int64,
|
||||
scheduledNotifications: [NotificationContent]
|
||||
) async {
|
||||
let timeDelta = abs(currentTime - lastKnownTime)
|
||||
|
||||
// Only handle significant time changes (>5 minutes)
|
||||
guard timeDelta > (5 * 60 * 1000) else {
|
||||
return // Ignore small clock adjustments
|
||||
}
|
||||
|
||||
// Recalculate all scheduled notifications
|
||||
for notification in scheduledNotifications {
|
||||
// Recalculate using original scheduled time
|
||||
let originalScheduledTime = notification.scheduledTime
|
||||
let newScheduledTime = recalculateScheduledTime(
|
||||
originalTime: originalScheduledTime,
|
||||
timeDelta: timeDelta
|
||||
)
|
||||
|
||||
// Cancel old notification
|
||||
await scheduler.cancelNotification(id: notification.id)
|
||||
|
||||
// Reschedule with corrected time
|
||||
let updatedNotification = NotificationContent(
|
||||
id: notification.id,
|
||||
title: notification.title,
|
||||
body: notification.body,
|
||||
scheduledTime: newScheduledTime,
|
||||
fetchedAt: notification.fetchedAt,
|
||||
url: notification.url,
|
||||
payload: notification.payload,
|
||||
etag: notification.etag
|
||||
)
|
||||
|
||||
await scheduler.scheduleNotification(updatedNotification)
|
||||
}
|
||||
|
||||
// Record time change in history
|
||||
await recordTimeChangeEvent(
|
||||
oldTime: lastKnownTime,
|
||||
newTime: currentTime,
|
||||
delta: timeDelta
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Strategy 2: Timezone Change Handling
|
||||
|
||||
**When Detected**:
|
||||
1. **Detect Timezone Change**
|
||||
- Compare current timezone with stored timezone
|
||||
- Detect on app launch, background tasks, rollover
|
||||
|
||||
2. **Recalculate All Scheduled Times**
|
||||
- Maintain same local time (e.g., 9:00 AM)
|
||||
- Convert to new timezone
|
||||
- Update scheduled times
|
||||
|
||||
3. **Reschedule All Notifications**
|
||||
- Cancel existing notifications
|
||||
- Schedule with new times
|
||||
- Update storage
|
||||
|
||||
**Implementation**:
|
||||
```swift
|
||||
func handleTimezoneChange(
|
||||
oldTimezone: TimeZone,
|
||||
newTimezone: TimeZone,
|
||||
scheduledNotifications: [NotificationContent]
|
||||
) async {
|
||||
// Extract local time from each notification
|
||||
for notification in scheduledNotifications {
|
||||
// Get local time components (hour, minute)
|
||||
let scheduledDate = notification.getScheduledTimeAsDate()
|
||||
let calendar = Calendar.current
|
||||
let hour = calendar.component(.hour, from: scheduledDate)
|
||||
let minute = calendar.component(.minute, from: scheduledDate)
|
||||
|
||||
// Recalculate in new timezone
|
||||
let newScheduledTime = calculateNextOccurrence(
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
timezone: newTimezone
|
||||
)
|
||||
|
||||
// Cancel old notification
|
||||
await scheduler.cancelNotification(id: notification.id)
|
||||
|
||||
// Reschedule with new time
|
||||
let updatedNotification = NotificationContent(
|
||||
id: notification.id,
|
||||
title: notification.title,
|
||||
body: notification.body,
|
||||
scheduledTime: newScheduledTime,
|
||||
fetchedAt: notification.fetchedAt,
|
||||
url: notification.url,
|
||||
payload: notification.payload,
|
||||
etag: notification.etag
|
||||
)
|
||||
|
||||
await scheduler.scheduleNotification(updatedNotification)
|
||||
}
|
||||
|
||||
// Update stored timezone
|
||||
await storage.saveTimezone(newTimezone.identifier)
|
||||
}
|
||||
```
|
||||
|
||||
### Strategy 3: DST Transition Handling
|
||||
|
||||
**When Detected**:
|
||||
1. **Use Calendar API**
|
||||
- `Calendar.date(byAdding: .hour, value: 24, to:)` handles DST automatically
|
||||
- No manual DST detection needed
|
||||
|
||||
2. **Validate Calculation**
|
||||
- Verify 24-hour addition results in correct local time
|
||||
- Log DST transitions for debugging
|
||||
- Handle edge cases (2:00 AM → 3:00 AM)
|
||||
|
||||
3. **Handle Edge Cases**
|
||||
- Spring forward: Notification might be scheduled for 2:00 AM (doesn't exist)
|
||||
- Fall back: Notification might be scheduled for 2:00 AM (occurs twice)
|
||||
- Use system's automatic handling
|
||||
|
||||
**Implementation**:
|
||||
```swift
|
||||
func calculateNextScheduledTime(_ currentScheduledTime: Int64) -> Int64 {
|
||||
let calendar = Calendar.current
|
||||
let currentDate = Date(timeIntervalSince1970: Double(currentScheduledTime) / 1000.0)
|
||||
|
||||
// Add 24 hours (handles DST automatically)
|
||||
guard let nextDate = calendar.date(byAdding: .hour, value: 24, to: currentDate) else {
|
||||
// Fallback to simple addition
|
||||
return currentScheduledTime + (24 * 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
// Validate: Ensure it's exactly 24 hours later in local time
|
||||
let currentHour = calendar.component(.hour, from: currentDate)
|
||||
let currentMinute = calendar.component(.minute, from: currentDate)
|
||||
let nextHour = calendar.component(.hour, from: nextDate)
|
||||
let nextMinute = calendar.component(.minute, from: nextDate)
|
||||
|
||||
// Log DST transitions
|
||||
if currentHour != nextHour || currentMinute != nextMinute {
|
||||
print("\(Self.TAG): DST transition detected: \(currentHour):\(currentMinute) -> \(nextHour):\(nextMinute)")
|
||||
}
|
||||
|
||||
return Int64(nextDate.timeIntervalSince1970 * 1000)
|
||||
}
|
||||
```
|
||||
|
||||
### Strategy 4: Duplicate Prevention
|
||||
|
||||
**Multi-Level Checks**:
|
||||
|
||||
1. **Rollover State Tracking**
|
||||
- Store rollover state in database
|
||||
- Track last processed notification ID
|
||||
- Prevent duplicate rollover attempts
|
||||
|
||||
2. **Time-Based Deduplication**
|
||||
- Check for existing notifications at same scheduled time
|
||||
- Use tolerance window (1 minute) for DST shifts
|
||||
- Compare notification IDs
|
||||
|
||||
3. **System-Level Verification**
|
||||
- Query `UNUserNotificationCenter` for pending notifications
|
||||
- Check if notification already scheduled
|
||||
- Cancel and reschedule if needed
|
||||
|
||||
**Implementation**:
|
||||
```swift
|
||||
func scheduleNextNotification(
|
||||
_ content: NotificationContent,
|
||||
storage: DailyNotificationStorage?,
|
||||
fetcher: DailyNotificationFetcher? = nil
|
||||
) async -> Bool {
|
||||
// Check 1: Rollover state tracking
|
||||
if let storage = storage {
|
||||
let lastRolloverTime = await storage.getLastRolloverTime(for: content.id)
|
||||
let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
|
||||
// If rollover was processed recently (< 1 hour ago), skip
|
||||
if let lastTime = lastRolloverTime,
|
||||
(currentTime - lastTime) < (60 * 60 * 1000) {
|
||||
print("\(Self.TAG): RESCHEDULE_SKIP id=\(content.id) already_processed")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate next time
|
||||
let nextScheduledTime = calculateNextScheduledTime(content.scheduledTime)
|
||||
|
||||
// Check 2: Storage-level duplicate check
|
||||
if let storage = storage {
|
||||
let existingNotifications = storage.getAllNotifications()
|
||||
let toleranceMs: Int64 = 60 * 1000 // 1 minute tolerance
|
||||
|
||||
for existing in existingNotifications {
|
||||
if abs(existing.scheduledTime - nextScheduledTime) <= toleranceMs {
|
||||
print("\(Self.TAG): RESCHEDULE_DUPLICATE id=\(content.id) existing_id=\(existing.id)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check 3: System-level duplicate check
|
||||
let pendingNotifications = await notificationCenter.pendingNotificationRequests()
|
||||
for pending in pendingNotifications {
|
||||
if let trigger = pending.trigger as? UNCalendarNotificationTrigger,
|
||||
let nextDate = trigger.nextTriggerDate() {
|
||||
let pendingTime = Int64(nextDate.timeIntervalSince1970 * 1000)
|
||||
let toleranceMs: Int64 = 60 * 1000
|
||||
|
||||
if abs(pendingTime - nextScheduledTime) <= toleranceMs {
|
||||
print("\(Self.TAG): RESCHEDULE_DUPLICATE id=\(content.id) system_pending_id=\(pending.identifier)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All checks passed, proceed with scheduling
|
||||
// ... (rest of scheduling logic)
|
||||
|
||||
// Mark rollover as processed
|
||||
await storage?.saveLastRolloverTime(for: content.id, time: Int64(Date().timeIntervalSince1970 * 1000))
|
||||
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
### Strategy 5: Race Condition Prevention
|
||||
|
||||
**Solution**: Use serial queue + state tracking
|
||||
|
||||
1. **Serial Queue for Rollover**
|
||||
- Use dedicated serial queue for rollover operations
|
||||
- Prevent concurrent rollover attempts
|
||||
- Ensure atomic operations
|
||||
|
||||
2. **State Machine**
|
||||
- Track rollover state (pending, processing, completed)
|
||||
- Prevent duplicate processing
|
||||
- Handle failures gracefully
|
||||
|
||||
3. **Locking Mechanism**
|
||||
- Use actor or serial queue for thread safety
|
||||
- Prevent race conditions
|
||||
- Ensure atomic updates
|
||||
|
||||
**Implementation**:
|
||||
```swift
|
||||
actor RolloverCoordinator {
|
||||
private var processingNotifications: Set<String> = []
|
||||
private let scheduler: DailyNotificationScheduler
|
||||
private let storage: DailyNotificationStorage
|
||||
|
||||
func processRollover(for notificationId: String) async -> Bool {
|
||||
// Check if already processing
|
||||
if processingNotifications.contains(notificationId) {
|
||||
print("RolloverCoordinator: Already processing \(notificationId)")
|
||||
return false
|
||||
}
|
||||
|
||||
// Mark as processing
|
||||
processingNotifications.insert(notificationId)
|
||||
defer {
|
||||
processingNotifications.remove(notificationId)
|
||||
}
|
||||
|
||||
// Perform rollover
|
||||
// ... (rollover logic)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Architecture
|
||||
|
||||
### Component 1: TimeChangeDetector
|
||||
|
||||
**Purpose**: Detect time changes and trigger recovery
|
||||
|
||||
**Responsibilities**:
|
||||
- Store last known system time
|
||||
- Compare on app launch/background tasks
|
||||
- Detect significant time jumps
|
||||
- Trigger time change recovery
|
||||
|
||||
**Location**: `ios/Plugin/DailyNotificationTimeChangeDetector.swift`
|
||||
|
||||
### Component 2: TimezoneChangeDetector
|
||||
|
||||
**Purpose**: Detect timezone changes and trigger recalculation
|
||||
|
||||
**Responsibilities**:
|
||||
- Store current timezone
|
||||
- Compare on access
|
||||
- Detect timezone changes
|
||||
- Trigger timezone change recovery
|
||||
|
||||
**Location**: `ios/Plugin/DailyNotificationTimezoneChangeDetector.swift`
|
||||
|
||||
### Component 3: RolloverCoordinator
|
||||
|
||||
**Purpose**: Coordinate rollover operations with duplicate prevention
|
||||
|
||||
**Responsibilities**:
|
||||
- Manage rollover state
|
||||
- Prevent duplicate rollovers
|
||||
- Coordinate multiple detection mechanisms
|
||||
- Handle race conditions
|
||||
|
||||
**Location**: `ios/Plugin/DailyNotificationRolloverCoordinator.swift`
|
||||
|
||||
### Component 4: Enhanced Recovery Manager
|
||||
|
||||
**Purpose**: Extend existing recovery manager with time/timezone change handling
|
||||
|
||||
**Responsibilities**:
|
||||
- Integrate time change detection
|
||||
- Integrate timezone change detection
|
||||
- Coordinate with rollover coordinator
|
||||
- Handle all edge cases
|
||||
|
||||
**Location**: `ios/Plugin/DailyNotificationReactivationManager.swift` (enhance existing)
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Test Category 1: Time Changes
|
||||
|
||||
1. **Manual Clock Adjustment**
|
||||
- Set device time forward 1 hour
|
||||
- Verify notifications rescheduled correctly
|
||||
- Verify rollover still works
|
||||
|
||||
2. **Clock Jump Forward**
|
||||
- Set device time forward 24 hours
|
||||
- Verify all notifications recalculated
|
||||
- Verify no duplicates created
|
||||
|
||||
3. **Clock Jump Backward**
|
||||
- Set device time backward 1 hour
|
||||
- Verify notifications still valid
|
||||
- Verify rollover works correctly
|
||||
|
||||
### Test Category 2: Timezone Changes
|
||||
|
||||
1. **Timezone Change**
|
||||
- Change device timezone
|
||||
- Verify notifications rescheduled to same local time
|
||||
- Verify rollover maintains local time
|
||||
|
||||
2. **Travel Simulation**
|
||||
- Change timezone multiple times
|
||||
- Verify notifications always at correct local time
|
||||
- Verify no duplicates
|
||||
|
||||
### Test Category 3: DST Transitions
|
||||
|
||||
1. **Spring Forward**
|
||||
- Test on DST spring forward day
|
||||
- Verify 24-hour calculation handles correctly
|
||||
- Verify notification fires at correct time
|
||||
|
||||
2. **Fall Back**
|
||||
- Test on DST fall back day
|
||||
- Verify 24-hour calculation handles correctly
|
||||
- Verify no duplicate notifications
|
||||
|
||||
### Test Category 4: Race Conditions
|
||||
|
||||
1. **Concurrent Rollover**
|
||||
- Trigger multiple rollover attempts simultaneously
|
||||
- Verify only one succeeds
|
||||
- Verify no duplicates
|
||||
|
||||
2. **App State Transitions**
|
||||
- Trigger rollover during app state changes
|
||||
- Verify rollover completes correctly
|
||||
- Verify no data corruption
|
||||
|
||||
### Test Category 5: Edge Cases
|
||||
|
||||
1. **Notification Limit**
|
||||
- Schedule 64 notifications
|
||||
- Verify rollover still works
|
||||
- Verify proper error handling
|
||||
|
||||
2. **Permission Changes**
|
||||
- Revoke notification permission
|
||||
- Verify graceful failure
|
||||
- Verify recovery when permission restored
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Core Rollover (Week 1)
|
||||
- ✅ DST-safe time calculation
|
||||
- ✅ Basic rollover scheduling
|
||||
- ✅ Duplicate prevention (storage + system level)
|
||||
- ✅ AppDelegate integration
|
||||
|
||||
### Phase 2: Edge Case Detection (Week 2)
|
||||
- ✅ Time change detection
|
||||
- ✅ Timezone change detection
|
||||
- ✅ Rollover state tracking
|
||||
- ✅ Race condition prevention
|
||||
|
||||
### Phase 3: Recovery Integration (Week 3)
|
||||
- ✅ Time change recovery
|
||||
- ✅ Timezone change recovery
|
||||
- ✅ Enhanced recovery manager
|
||||
- ✅ Background task integration
|
||||
|
||||
### Phase 4: Testing & Validation (Week 4)
|
||||
- ✅ Comprehensive edge case testing
|
||||
- ✅ Real device testing
|
||||
- ✅ DST transition testing
|
||||
- ✅ Performance optimization
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. **Reliability**: 99%+ rollover success rate across all edge cases
|
||||
2. **No Duplicates**: Zero duplicate notifications in any scenario
|
||||
3. **Time Accuracy**: Notifications fire within 1 minute of scheduled time
|
||||
4. **Recovery**: All edge cases handled gracefully with recovery
|
||||
5. **Performance**: Rollover completes in <1 second
|
||||
6. **Logging**: Comprehensive logging for debugging
|
||||
|
||||
---
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
### Risk 1: iOS Background Execution Limits
|
||||
**Mitigation**: Multiple detection mechanisms (delegate + background + recovery)
|
||||
|
||||
### Risk 2: Time Change Detection Reliability
|
||||
**Mitigation**: Store timestamps, compare on every access, validate scheduled times
|
||||
|
||||
### Risk 3: Race Conditions
|
||||
**Mitigation**: Serial queue, state machine, actor-based coordination
|
||||
|
||||
### Risk 4: DST Edge Cases
|
||||
**Mitigation**: Use Calendar API, validate calculations, comprehensive testing
|
||||
|
||||
### Risk 5: Notification System Limits
|
||||
**Mitigation**: Check pending count, handle gracefully, provide user feedback
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Review & Approve Plan** (This document)
|
||||
2. **Create Implementation Tasks** (Break down into specific tasks)
|
||||
3. **Implement Phase 1** (Core rollover functionality)
|
||||
4. **Test Phase 1** (Basic functionality)
|
||||
5. **Implement Phase 2** (Edge case detection)
|
||||
6. **Test Phase 2** (Edge case scenarios)
|
||||
7. **Implement Phase 3** (Recovery integration)
|
||||
8. **Test Phase 3** (Recovery scenarios)
|
||||
9. **Final Testing** (Comprehensive validation)
|
||||
10. **Documentation** (Update docs with edge case handling)
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Android Implementation: `DailyNotificationWorker.java` (scheduleNextNotification)
|
||||
- Android Time Change Handling: `DailyNotificationRebootRecoveryManager.java`
|
||||
- iOS Calendar API: `Calendar.date(byAdding:to:)` documentation
|
||||
- iOS Background Tasks: `BGTaskScheduler` documentation
|
||||
- iOS Notifications: `UNUserNotificationCenter` documentation
|
||||
633
doc/platform/ios/ROLLOVER_IMPLEMENTATION_REVIEW.md
Normal file
633
doc/platform/ios/ROLLOVER_IMPLEMENTATION_REVIEW.md
Normal file
@@ -0,0 +1,633 @@
|
||||
# iOS Rollover Implementation — Comprehensive Review
|
||||
|
||||
**Status**: Pre-Implementation Review
|
||||
**Date**: 2025-01-27
|
||||
**Priority**: Reliability-First
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Plan Overview](#plan-overview)
|
||||
2. [File Changes Summary](#file-changes-summary)
|
||||
3. [Detailed File Modifications](#detailed-file-modifications)
|
||||
4. [Integration Points](#integration-points)
|
||||
5. [Dependencies & Order](#dependencies--order)
|
||||
6. [Testing Strategy](#testing-strategy)
|
||||
7. [Open Questions](#open-questions)
|
||||
|
||||
---
|
||||
|
||||
## Plan Overview
|
||||
|
||||
### Objective
|
||||
Implement Android-like automatic rollover for iOS notifications with comprehensive edge case handling.
|
||||
|
||||
### Key Features
|
||||
- ✅ Automatic rollover when notification fires (24 hours later)
|
||||
- ✅ DST-safe time calculations
|
||||
- ✅ Multi-level duplicate prevention
|
||||
- ✅ Time/timezone change detection and recovery
|
||||
- ✅ Race condition prevention
|
||||
- ✅ Comprehensive edge case handling
|
||||
|
||||
### Architecture Components
|
||||
1. **TimeChangeDetector** — Detects time changes
|
||||
2. **TimezoneChangeDetector** — Detects timezone changes
|
||||
3. **RolloverCoordinator** — Coordinates rollover operations
|
||||
4. **Enhanced Recovery Manager** — Integrates all edge case handling
|
||||
|
||||
---
|
||||
|
||||
## File Changes Summary
|
||||
|
||||
| File | Change Type | Lines Added | Purpose |
|
||||
|------|-------------|-------------|---------|
|
||||
| `DailyNotificationScheduler.swift` | Add methods | ~150 | DST-safe calculation + rollover scheduling |
|
||||
| `DailyNotificationPlugin.swift` | Add method | ~50 | Rollover handler entry point |
|
||||
| `AppDelegate.swift` | Modify method | ~20 | Detect notification delivery (foreground) |
|
||||
| `DailyNotificationReactivationManager.swift` | Enhance | ~100 | Rollover on app launch recovery |
|
||||
| `DailyNotificationStorage.swift` | Add methods | ~30 | Rollover state tracking |
|
||||
| `DailyNotificationTimeChangeDetector.swift` | New file | ~200 | Time change detection |
|
||||
| `DailyNotificationTimezoneChangeDetector.swift` | New file | ~150 | Timezone change detection |
|
||||
| `DailyNotificationRolloverCoordinator.swift` | New file | ~250 | Rollover coordination |
|
||||
|
||||
**Total**: ~950 lines of new/modified code
|
||||
|
||||
---
|
||||
|
||||
## Detailed File Modifications
|
||||
|
||||
### 1. DailyNotificationScheduler.swift
|
||||
|
||||
**Location**: `ios/Plugin/DailyNotificationScheduler.swift`
|
||||
|
||||
#### Change 1.1: Add DST-Safe Next Time Calculation
|
||||
|
||||
**Insert after line 307** (after `calculateNextOccurrence` method):
|
||||
|
||||
```swift
|
||||
/**
|
||||
* Calculate next scheduled time from current scheduled time (24 hours later, DST-safe)
|
||||
*
|
||||
* Matches Android calculateNextScheduledTime() functionality
|
||||
* Handles DST transitions automatically using Calendar
|
||||
*
|
||||
* @param currentScheduledTime Current scheduled time in milliseconds
|
||||
* @return Next scheduled time in milliseconds (24 hours later)
|
||||
*/
|
||||
func calculateNextScheduledTime(_ currentScheduledTime: Int64) -> Int64 {
|
||||
let calendar = Calendar.current
|
||||
let currentDate = Date(timeIntervalSince1970: Double(currentScheduledTime) / 1000.0)
|
||||
|
||||
// Add 24 hours (handles DST transitions automatically)
|
||||
guard let nextDate = calendar.date(byAdding: .hour, value: 24, to: currentDate) else {
|
||||
// Fallback to simple 24-hour addition if calendar calculation fails
|
||||
print("\(Self.TAG): DST calculation failed, using fallback")
|
||||
return currentScheduledTime + (24 * 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
// Validate: Log DST transitions for debugging
|
||||
let currentHour = calendar.component(.hour, from: currentDate)
|
||||
let currentMinute = calendar.component(.minute, from: currentDate)
|
||||
let nextHour = calendar.component(.hour, from: nextDate)
|
||||
let nextMinute = calendar.component(.minute, from: nextDate)
|
||||
|
||||
if currentHour != nextHour || currentMinute != nextMinute {
|
||||
print("\(Self.TAG): DST transition detected: \(currentHour):\(String(format: "%02d", currentMinute)) -> \(nextHour):\(String(format: "%02d", nextMinute))")
|
||||
}
|
||||
|
||||
return Int64(nextDate.timeIntervalSince1970 * 1000)
|
||||
}
|
||||
```
|
||||
|
||||
#### Change 1.2: Add Rollover Scheduling Method
|
||||
|
||||
**Insert after line 202** (after `scheduleNotification` method):
|
||||
|
||||
```swift
|
||||
/**
|
||||
* Schedule next notification after current one fires (rollover)
|
||||
*
|
||||
* Matches Android scheduleNextNotification() functionality
|
||||
* Implements multi-level duplicate prevention
|
||||
*
|
||||
* @param content Current notification content that just fired
|
||||
* @param storage Storage instance for duplicate checking
|
||||
* @param fetcher Optional fetcher for scheduling prefetch
|
||||
* @return true if next notification was scheduled successfully
|
||||
*/
|
||||
func scheduleNextNotification(
|
||||
_ content: NotificationContent,
|
||||
storage: DailyNotificationStorage?,
|
||||
fetcher: DailyNotificationFetcher? = nil
|
||||
) async -> Bool {
|
||||
print("\(Self.TAG): RESCHEDULE_START id=\(content.id)")
|
||||
|
||||
// Check 1: Rollover state tracking (prevent duplicate rollover attempts)
|
||||
if let storage = storage {
|
||||
let lastRolloverTime = await storage.getLastRolloverTime(for: content.id)
|
||||
let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
|
||||
// If rollover was processed recently (< 1 hour ago), skip
|
||||
if let lastTime = lastRolloverTime,
|
||||
(currentTime - lastTime) < (60 * 60 * 1000) {
|
||||
print("\(Self.TAG): RESCHEDULE_SKIP id=\(content.id) already_processed")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate next occurrence using DST-safe calculation
|
||||
let nextScheduledTime = calculateNextScheduledTime(content.scheduledTime)
|
||||
|
||||
// Check 2: Storage-level duplicate check (prevent duplicate notifications)
|
||||
if let storage = storage {
|
||||
let existingNotifications = storage.getAllNotifications()
|
||||
let toleranceMs: Int64 = 60 * 1000 // 1 minute tolerance for DST shifts
|
||||
|
||||
for existing in existingNotifications {
|
||||
if abs(existing.scheduledTime - nextScheduledTime) <= toleranceMs {
|
||||
print("\(Self.TAG): RESCHEDULE_DUPLICATE id=\(content.id) existing_id=\(existing.id) time_diff_ms=\(abs(existing.scheduledTime - nextScheduledTime))")
|
||||
return false // Skip rescheduling to prevent duplicate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check 3: System-level duplicate check (query UNUserNotificationCenter)
|
||||
let pendingNotifications = await notificationCenter.pendingNotificationRequests()
|
||||
for pending in pendingNotifications {
|
||||
if let trigger = pending.trigger as? UNCalendarNotificationTrigger,
|
||||
let nextDate = trigger.nextTriggerDate() {
|
||||
let pendingTime = Int64(nextDate.timeIntervalSince1970 * 1000)
|
||||
let toleranceMs: Int64 = 60 * 1000
|
||||
|
||||
if abs(pendingTime - nextScheduledTime) <= toleranceMs {
|
||||
print("\(Self.TAG): RESCHEDULE_DUPLICATE id=\(content.id) system_pending_id=\(pending.identifier)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract hour:minute from current scheduled time for logging
|
||||
let calendar = Calendar.current
|
||||
let scheduledDate = content.getScheduledTimeAsDate()
|
||||
let hour = calendar.component(.hour, from: scheduledDate)
|
||||
let minute = calendar.component(.minute, from: scheduledDate)
|
||||
|
||||
// Create new notification content for next occurrence
|
||||
// Note: Content will be refreshed by prefetch, but we need placeholder
|
||||
let nextId = "daily_rollover_\(Int64(Date().timeIntervalSince1970 * 1000))"
|
||||
let nextContent = NotificationContent(
|
||||
id: nextId,
|
||||
title: content.title, // Will be updated by prefetch
|
||||
body: content.body, // Will be updated by prefetch
|
||||
scheduledTime: nextScheduledTime,
|
||||
fetchedAt: Int64(Date().timeIntervalSince1970 * 1000),
|
||||
url: content.url,
|
||||
payload: content.payload,
|
||||
etag: content.etag
|
||||
)
|
||||
|
||||
// Schedule the next notification
|
||||
let scheduled = await scheduleNotification(nextContent)
|
||||
|
||||
if scheduled {
|
||||
let nextTimeStr = formatTime(nextScheduledTime)
|
||||
print("\(Self.TAG): RESCHEDULE_OK id=\(content.id) next=\(nextTimeStr) nextId=\(nextId)")
|
||||
|
||||
// Schedule background fetch for next notification (5 minutes before scheduled time)
|
||||
// Note: DailyNotificationFetcher integration deferred to Phase 2
|
||||
if let fetcher = fetcher {
|
||||
let fetchTime = nextScheduledTime - (5 * 60 * 1000) // 5 minutes before
|
||||
let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
|
||||
if fetchTime > currentTime {
|
||||
// TODO: Phase 2 - Implement fetcher.scheduleFetch(fetchTime)
|
||||
print("\(Self.TAG): RESCHEDULE_PREFETCH_SCHEDULED id=\(content.id) next_fetch=\(fetchTime) next_notification=\(nextScheduledTime)")
|
||||
} else {
|
||||
// TODO: Phase 2 - Implement fetcher.scheduleImmediateFetch()
|
||||
print("\(Self.TAG): RESCHEDULE_PREFETCH_PAST id=\(content.id) fetch_time=\(fetchTime) current=\(currentTime)")
|
||||
}
|
||||
} else {
|
||||
print("\(Self.TAG): RESCHEDULE_PREFETCH_SKIP id=\(content.id) fetcher_not_available")
|
||||
}
|
||||
|
||||
// Mark rollover as processed
|
||||
await storage?.saveLastRolloverTime(for: content.id, time: Int64(Date().timeIntervalSince1970 * 1000))
|
||||
|
||||
return true
|
||||
} else {
|
||||
print("\(Self.TAG): RESCHEDULE_ERR id=\(content.id) scheduling_failed")
|
||||
return false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: The `formatTime` method already exists (line 273), so no change needed there.
|
||||
|
||||
---
|
||||
|
||||
### 2. DailyNotificationPlugin.swift
|
||||
|
||||
**Location**: `ios/Plugin/DailyNotificationPlugin.swift`
|
||||
|
||||
#### Change 2.1: Add Rollover Handler Method + Notification Observer
|
||||
|
||||
**Insert after line 77** (in `load()` method, after recovery manager initialization):
|
||||
|
||||
```swift
|
||||
// Register for notification delivery events (Notification Center pattern)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleNotificationDelivery(_:)),
|
||||
name: NSNotification.Name("DailyNotificationDelivered"),
|
||||
object: nil
|
||||
)
|
||||
```
|
||||
|
||||
**Insert after line 1242** (after `getNotificationStatus` method):
|
||||
|
||||
```swift
|
||||
/**
|
||||
* Handle notification delivery event (from Notification Center)
|
||||
*
|
||||
* This is called when AppDelegate posts notification delivery event
|
||||
* Matches Android's scheduleNextNotification() behavior
|
||||
*
|
||||
* @param notification NSNotification with userInfo containing notification_id and scheduled_time
|
||||
*/
|
||||
@objc private func handleNotificationDelivery(_ notification: Notification) {
|
||||
guard let userInfo = notification.userInfo,
|
||||
let notificationId = userInfo["notification_id"] as? String,
|
||||
let scheduledTime = userInfo["scheduled_time"] as? Int64 else {
|
||||
print("DNP-ROLLOVER: Invalid notification data")
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
await processRollover(notificationId: notificationId, scheduledTime: scheduledTime)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process rollover for delivered notification
|
||||
*
|
||||
* @param notificationId ID of notification that was delivered
|
||||
* @param scheduledTime Scheduled time of delivered notification
|
||||
*/
|
||||
private func processRollover(notificationId: String, scheduledTime: Int64) async {
|
||||
guard let scheduler = scheduler, let storage = storage else {
|
||||
print("DNP-ROLLOVER: Plugin not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the notification content that was delivered
|
||||
guard let content = storage.getNotificationContent(id: notificationId) else {
|
||||
print("DNP-ROLLOVER: Could not find notification content for id=\(notificationId)")
|
||||
return
|
||||
}
|
||||
|
||||
// Schedule next notification
|
||||
// Note: DailyNotificationFetcher integration deferred to Phase 2
|
||||
let scheduled = await scheduler.scheduleNextNotification(
|
||||
content,
|
||||
storage: storage,
|
||||
fetcher: nil // TODO: Phase 2 - Add fetcher instance
|
||||
)
|
||||
|
||||
if scheduled {
|
||||
print("DNP-ROLLOVER: Successfully scheduled next notification for id=\(notificationId)")
|
||||
// Log success (non-fatal, background operation)
|
||||
} else {
|
||||
print("DNP-ROLLOVER: Failed to schedule next notification for id=\(notificationId)")
|
||||
// Log failure but continue (recovery will handle on next launch)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Change 2.2: Update getNotificationStatus to Include Rollover Info
|
||||
|
||||
**Modify line 1229-1236** (in `getNotificationStatus` method):
|
||||
|
||||
```swift
|
||||
// Calculate next notification time
|
||||
let nextNotificationTime = await scheduler.getNextNotificationTime() ?? 0
|
||||
|
||||
// Get rollover status
|
||||
let lastRolloverTime = await storage?.getLastRolloverTime() ?? 0
|
||||
|
||||
var result: [String: Any] = [
|
||||
"isEnabled": isEnabled,
|
||||
"isScheduled": pendingCount > 0,
|
||||
"lastNotificationTime": lastNotification?.scheduledTime ?? 0,
|
||||
"nextNotificationTime": nextNotificationTime,
|
||||
"pending": pendingCount,
|
||||
"rolloverEnabled": true, // Indicate rollover is active
|
||||
"lastRolloverTime": lastRolloverTime, // When last rollover occurred
|
||||
"settings": settings
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. AppDelegate.swift
|
||||
|
||||
**Location**: `test-apps/ios-test-app/ios/App/App/AppDelegate.swift`
|
||||
|
||||
#### Change 3.1: Modify willPresent to Trigger Rollover
|
||||
|
||||
**Replace lines 136-152** with:
|
||||
|
||||
```swift
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
NSLog("DNP-DEBUG: ✅ userNotificationCenter willPresent called!")
|
||||
NSLog("DNP-DEBUG: Notification received in foreground: %@", notification.request.identifier)
|
||||
NSLog("DNP-DEBUG: Notification title: %@", notification.request.content.title)
|
||||
NSLog("DNP-DEBUG: Notification body: %@", notification.request.content.body)
|
||||
|
||||
// Extract notification info from userInfo for rollover
|
||||
let userInfo = notification.request.content.userInfo
|
||||
if let notificationId = userInfo["notification_id"] as? String,
|
||||
let scheduledTime = userInfo["scheduled_time"] as? Int64 {
|
||||
|
||||
// Trigger rollover scheduling (async, non-blocking)
|
||||
Task {
|
||||
await handleNotificationRollover(notificationId: notificationId, scheduledTime: scheduledTime)
|
||||
}
|
||||
}
|
||||
|
||||
// Show notification with banner, sound, and badge
|
||||
// Use .banner for iOS 14+, fallback to .alert for iOS 13
|
||||
if #available(iOS 14.0, *) {
|
||||
completionHandler([.banner, .sound, .badge])
|
||||
} else {
|
||||
completionHandler([.alert, .sound, .badge])
|
||||
}
|
||||
|
||||
NSLog("DNP-DEBUG: ✅ Completion handler called with presentation options")
|
||||
}
|
||||
```
|
||||
|
||||
#### Change 3.2: Post Notification for Rollover (Notification Center Pattern)
|
||||
|
||||
**Insert after line 152** (after `willPresent` completion handler):
|
||||
|
||||
```swift
|
||||
// Post notification to trigger rollover (decoupled pattern)
|
||||
NotificationCenter.default.post(
|
||||
name: NSNotification.Name("DailyNotificationDelivered"),
|
||||
object: nil,
|
||||
userInfo: [
|
||||
"notification_id": notificationId,
|
||||
"scheduled_time": scheduledTime
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
**Note**: This uses Notification Center pattern for decoupling. Plugin will observe this notification.
|
||||
|
||||
---
|
||||
|
||||
### 4. DailyNotificationStorage.swift
|
||||
|
||||
**Location**: `ios/Plugin/DailyNotificationStorage.swift`
|
||||
|
||||
#### Change 4.1: Add Rollover State Tracking Methods
|
||||
|
||||
**Insert after line 148** (after `getAllNotifications` method):
|
||||
|
||||
```swift
|
||||
/**
|
||||
* Get last rollover time for a notification ID
|
||||
*
|
||||
* @param notificationId Notification ID
|
||||
* @return Last rollover time in milliseconds, or nil if never rolled over
|
||||
*/
|
||||
func getLastRolloverTime(for notificationId: String) async -> Int64? {
|
||||
let key = "rollover_\(notificationId)"
|
||||
let lastTime = userDefaults.object(forKey: key) as? Int64
|
||||
return lastTime
|
||||
}
|
||||
|
||||
/**
|
||||
* Save last rollover time for a notification ID
|
||||
*
|
||||
* @param notificationId Notification ID
|
||||
* @param time Rollover time in milliseconds
|
||||
*/
|
||||
func saveLastRolloverTime(for notificationId: String, time: Int64) async {
|
||||
let key = "rollover_\(notificationId)"
|
||||
userDefaults.set(time, forKey: key)
|
||||
userDefaults.synchronize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last rollover time (any notification)
|
||||
*
|
||||
* @return Last rollover time in milliseconds, or 0 if never rolled over
|
||||
*/
|
||||
func getLastRolloverTime() -> Int64 {
|
||||
let key = "rollover_last"
|
||||
return Int64(userDefaults.integer(forKey: key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Save last rollover time (any notification)
|
||||
*
|
||||
* @param time Rollover time in milliseconds
|
||||
*/
|
||||
func saveLastRolloverTime(_ time: Int64) {
|
||||
let key = "rollover_last"
|
||||
userDefaults.set(time, forKey: key)
|
||||
userDefaults.synchronize()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. DailyNotificationReactivationManager.swift
|
||||
|
||||
**Location**: `ios/Plugin/DailyNotificationReactivationManager.swift`
|
||||
|
||||
#### Change 5.1: Add Rollover Check to Recovery
|
||||
|
||||
**Insert after line 338** (in `performColdStartRecovery` method, after detecting missed notifications):
|
||||
|
||||
```swift
|
||||
// Step 4.5: Check for delivered notifications and trigger rollover
|
||||
// This handles notifications that were delivered while app was not running
|
||||
await checkAndProcessDeliveredNotifications()
|
||||
```
|
||||
|
||||
#### Change 5.2: Add Delivered Notifications Check Method
|
||||
|
||||
**Insert at end of class** (before closing brace):
|
||||
|
||||
```swift
|
||||
/**
|
||||
* Check for delivered notifications and trigger rollover
|
||||
*
|
||||
* This ensures rollover happens on app launch if notifications were delivered
|
||||
* while the app was not running
|
||||
*/
|
||||
private func checkAndProcessDeliveredNotifications() async {
|
||||
print("\(Self.TAG): Checking for delivered notifications to trigger rollover")
|
||||
|
||||
// Get delivered notifications from system
|
||||
let deliveredNotifications = await notificationCenter.deliveredNotifications()
|
||||
|
||||
// Get last processed rollover time from storage
|
||||
let lastProcessedTime = storage.getLastRolloverTime()
|
||||
|
||||
for notification in deliveredNotifications {
|
||||
let userInfo = notification.request.content.userInfo
|
||||
|
||||
guard let notificationId = userInfo["notification_id"] as? String,
|
||||
let scheduledTime = userInfo["scheduled_time"] as? Int64 else {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only process if this notification hasn't been processed yet
|
||||
if scheduledTime > lastProcessedTime {
|
||||
print("\(Self.TAG): Found delivered notification id=\(notificationId) scheduledTime=\(scheduledTime)")
|
||||
|
||||
// Get notification content
|
||||
guard let content = storage.getNotificationContent(id: notificationId) else {
|
||||
print("\(Self.TAG): Could not find content for delivered notification id=\(notificationId)")
|
||||
continue
|
||||
}
|
||||
|
||||
// Trigger rollover
|
||||
let scheduled = await scheduler.scheduleNextNotification(
|
||||
content,
|
||||
storage: storage,
|
||||
fetcher: nil // TODO: Add fetcher in Phase 2
|
||||
)
|
||||
|
||||
if scheduled {
|
||||
print("\(Self.TAG): Successfully rolled over delivered notification id=\(notificationId)")
|
||||
// Update last processed time
|
||||
storage.saveLastRolloverTime(scheduledTime)
|
||||
} else {
|
||||
print("\(Self.TAG): Failed to roll over delivered notification id=\(notificationId)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### 1. AppDelegate → Plugin (Notification Center Pattern)
|
||||
- **Flow**: AppDelegate detects notification → posts Notification Center event → plugin observes and handles
|
||||
- **Challenge**: Decoupling AppDelegate from plugin
|
||||
- **Solution**: Use Notification Center for decoupled communication
|
||||
|
||||
### 2. Plugin → Scheduler
|
||||
- **Flow**: Plugin receives rollover request → calls scheduler method
|
||||
- **Challenge**: Passing storage and fetcher instances
|
||||
- **Solution**: Plugin maintains references, passes to scheduler
|
||||
|
||||
### 3. Scheduler → Storage
|
||||
- **Flow**: Scheduler checks duplicates → queries storage
|
||||
- **Challenge**: Thread safety
|
||||
- **Solution**: Storage methods are already thread-safe (UserDefaults)
|
||||
|
||||
### 4. Recovery Manager → Scheduler
|
||||
- **Flow**: Recovery detects delivered notifications → triggers rollover
|
||||
- **Challenge**: Ensuring rollover happens on app launch
|
||||
- **Solution**: Integrate into existing recovery flow
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Order
|
||||
|
||||
### Implementation Order
|
||||
|
||||
1. **Phase 1: Core Infrastructure**
|
||||
- ✅ Add `calculateNextScheduledTime` to Scheduler
|
||||
- ✅ Add `scheduleNextNotification` to Scheduler
|
||||
- ✅ Add rollover state tracking to Storage
|
||||
- ✅ Add `handleNotificationRollover` to Plugin
|
||||
|
||||
2. **Phase 2: Detection Mechanisms**
|
||||
- ✅ Modify AppDelegate `willPresent` method
|
||||
- ✅ Add rollover check to Recovery Manager
|
||||
- ✅ Test foreground delivery
|
||||
|
||||
3. **Phase 3: Edge Case Handling** (Future)
|
||||
- Add TimeChangeDetector
|
||||
- Add TimezoneChangeDetector
|
||||
- Add RolloverCoordinator
|
||||
|
||||
4. **Phase 4: Integration** (Future)
|
||||
- Integrate fetcher for prefetch scheduling
|
||||
- Add comprehensive logging
|
||||
- Performance optimization
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Test 1: Foreground Delivery
|
||||
- **Setup**: App running, notification fires
|
||||
- **Expected**: Rollover triggers via AppDelegate → Notification Center → Plugin
|
||||
- **Verify**: Next notification scheduled, logs show rollover success
|
||||
|
||||
### Test 2: Background Delivery
|
||||
- **Setup**: App not running, notification fires
|
||||
- **Expected**: Rollover triggers on app launch via Recovery Manager
|
||||
- **Verify**: Next notification scheduled, recovery logs show rollover
|
||||
|
||||
### Test 3: Duplicate Prevention
|
||||
- **Setup**: Trigger rollover multiple times (rapid fire)
|
||||
- **Expected**: Only one notification scheduled
|
||||
- **Verify**: No duplicates in system, logs show duplicate prevention
|
||||
|
||||
### Test 4: DST Transition
|
||||
- **Setup**: Schedule notification on DST transition day
|
||||
- **Expected**: 24-hour calculation handles DST correctly
|
||||
- **Verify**: Notification fires at correct time, logs show DST transition
|
||||
|
||||
### Test 5: Error Handling
|
||||
- **Setup**: Simulate failure (e.g., invalid notification ID)
|
||||
- **Expected**: Error logged, app continues, no crash
|
||||
- **Verify**: Logs show error, recovery handles on next launch
|
||||
|
||||
---
|
||||
|
||||
## Open Questions — RESOLVED
|
||||
|
||||
**See**: `docs/ios-rollover-open-questions-answers.md` for detailed answers
|
||||
|
||||
### Summary of Decisions:
|
||||
|
||||
1. **Fetcher Integration**: ✅ Defer to Phase 2, use optional parameter pattern
|
||||
2. **AppDelegate Access**: ✅ Use Notification Center pattern (decoupling, flexibility)
|
||||
3. **Background Task**: ✅ Rely on existing recovery + AppDelegate (no dedicated task)
|
||||
4. **Error Handling**: ✅ Log + Continue (non-fatal), no retry, no user notification
|
||||
5. **Performance**: ✅ Process individually (low volume, simplicity)
|
||||
6. **Testing**: ✅ Manual testing for Phase 1, automated tests for Phase 2
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Review this document** ✅ (Current step)
|
||||
2. **Address open questions**
|
||||
3. **Create implementation tasks**
|
||||
4. **Implement Phase 1** (Core rollover)
|
||||
5. **Test Phase 1**
|
||||
6. **Implement Phase 2** (Edge case detection)
|
||||
7. **Final testing and validation**
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Edge Case Plan: `docs/ios-rollover-edge-case-plan.md`
|
||||
- Android Implementation: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java`
|
||||
- iOS Scheduler: `ios/Plugin/DailyNotificationScheduler.swift`
|
||||
- iOS Plugin: `ios/Plugin/DailyNotificationPlugin.swift`
|
||||
343
doc/platform/ios/ROLLOVER_QA.md
Normal file
343
doc/platform/ios/ROLLOVER_QA.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# iOS Rollover Implementation — Open Questions & Answers
|
||||
|
||||
**Date**: 2025-01-27
|
||||
**Status**: Pre-Implementation Decisions
|
||||
|
||||
---
|
||||
|
||||
## Question 1: Fetcher Integration
|
||||
|
||||
**Question**: How should we integrate DailyNotificationFetcher for prefetch scheduling? (Phase 2)
|
||||
|
||||
### Current State Analysis
|
||||
|
||||
- **Android**: Uses `DailyNotificationFetcher.scheduleFetch(fetchTime)` and `scheduleImmediateFetch()`
|
||||
- **iOS**: Has `DailyNotificationBackgroundTaskManager` with `scheduleBackgroundTask()` method
|
||||
- **iOS Pattern**: Uses `BGTaskScheduler` with `BGAppRefreshTaskRequest`
|
||||
|
||||
### Recommendation: **Defer to Phase 2, Use Placeholder Pattern**
|
||||
|
||||
**Rationale**:
|
||||
1. **Phase 1 Focus**: Core rollover functionality (scheduling next notification)
|
||||
2. **Prefetch is Separate**: Prefetch scheduling is independent of rollover
|
||||
3. **Existing Infrastructure**: iOS already has background task infrastructure
|
||||
4. **Android Pattern**: Android also separates rollover from prefetch (optional parameter)
|
||||
|
||||
### Implementation Approach
|
||||
|
||||
**Phase 1 (Current)**:
|
||||
- Make `fetcher` parameter optional in `scheduleNextNotification()`
|
||||
- Add TODO comments for Phase 2 integration
|
||||
- Log prefetch scheduling intent (even if not executed)
|
||||
|
||||
**Phase 2 (Future)**:
|
||||
- Create `DailyNotificationFetcher` class (iOS equivalent)
|
||||
- Integrate with `DailyNotificationBackgroundTaskManager`
|
||||
- Use `BGTaskScheduler` for prefetch scheduling
|
||||
- Calculate fetch time: `nextScheduledTime - (5 * 60 * 1000)` (5 minutes before)
|
||||
|
||||
### Code Pattern
|
||||
|
||||
```swift
|
||||
// Phase 1: Optional fetcher, log intent
|
||||
if let fetcher = fetcher {
|
||||
let fetchTime = nextScheduledTime - (5 * 60 * 1000)
|
||||
// TODO: Phase 2 - Implement fetcher.scheduleFetch(fetchTime)
|
||||
print("\(Self.TAG): RESCHEDULE_PREFETCH_SCHEDULED id=\(content.id) next_fetch=\(fetchTime)")
|
||||
} else {
|
||||
print("\(Self.TAG): RESCHEDULE_PREFETCH_SKIP id=\(content.id) fetcher_not_available")
|
||||
}
|
||||
```
|
||||
|
||||
**Decision**: ✅ **Defer to Phase 2, use optional parameter pattern**
|
||||
|
||||
---
|
||||
|
||||
## Question 2: AppDelegate Access
|
||||
|
||||
**Question**: Is there a better way to access the plugin from AppDelegate without using Capacitor bridge?
|
||||
|
||||
### Current State Analysis
|
||||
|
||||
- **Capacitor Pattern**: Uses `CAPBridgeViewController` to access plugins
|
||||
- **Test App**: Already uses this pattern for other operations
|
||||
- **Production Apps**: May have different AppDelegate structures
|
||||
|
||||
### Recommendation: **Use Notification Center Pattern**
|
||||
|
||||
**Rationale**:
|
||||
1. **Decoupling**: AppDelegate doesn't need direct plugin reference
|
||||
2. **Flexibility**: Works across different app architectures
|
||||
3. **Reliability**: Notification center is always available
|
||||
4. **Testability**: Easier to test without Capacitor dependency
|
||||
|
||||
### Implementation Approach
|
||||
|
||||
**Option A: Notification Center (Recommended)**
|
||||
- Plugin registers for notification delivery events
|
||||
- AppDelegate posts notification when delivery detected
|
||||
- Plugin handles rollover in response to notification
|
||||
|
||||
**Option B: Capacitor Bridge (Fallback)**
|
||||
- Use existing bridge pattern
|
||||
- Works but creates tight coupling
|
||||
- Use as fallback if notification center doesn't work
|
||||
|
||||
### Code Pattern
|
||||
|
||||
```swift
|
||||
// In DailyNotificationPlugin.load():
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleNotificationDelivery(_:)),
|
||||
name: NSNotification.Name("DailyNotificationDelivered"),
|
||||
object: nil
|
||||
)
|
||||
|
||||
// In AppDelegate.willPresent:
|
||||
NotificationCenter.default.post(
|
||||
name: NSNotification.Name("DailyNotificationDelivered"),
|
||||
object: nil,
|
||||
userInfo: [
|
||||
"notification_id": notificationId,
|
||||
"scheduled_time": scheduledTime
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
**Decision**: ✅ **Use Notification Center pattern, with Capacitor bridge as fallback**
|
||||
|
||||
---
|
||||
|
||||
## Question 3: Background Task
|
||||
|
||||
**Question**: Should we add a dedicated background task for rollover detection, or rely on existing recovery mechanisms?
|
||||
|
||||
### Current State Analysis
|
||||
|
||||
- **Existing Recovery**: `DailyNotificationReactivationManager` already runs on app launch
|
||||
- **Background Tasks**: iOS has strict limits on background execution
|
||||
- **Reliability**: Multiple detection mechanisms increase reliability
|
||||
|
||||
### Recommendation: **Rely on Existing Recovery + AppDelegate**
|
||||
|
||||
**Rationale**:
|
||||
1. **iOS Limitations**: Background tasks are unreliable (system-controlled)
|
||||
2. **Existing Infrastructure**: Recovery manager already handles app launch scenarios
|
||||
3. **Coverage**: AppDelegate (foreground) + Recovery (background) covers all cases
|
||||
4. **Simplicity**: Fewer moving parts = fewer failure points
|
||||
|
||||
### Implementation Approach
|
||||
|
||||
**Two Detection Mechanisms**:
|
||||
1. **Foreground**: AppDelegate `willPresent` → immediate rollover
|
||||
2. **Background**: Recovery Manager → rollover on app launch
|
||||
|
||||
**No Dedicated Background Task**:
|
||||
- Background tasks are unreliable (system decides when to run)
|
||||
- Recovery manager already covers app launch scenarios
|
||||
- Adding another mechanism adds complexity without significant benefit
|
||||
|
||||
### Code Pattern
|
||||
|
||||
```swift
|
||||
// Detection Mechanism 1: Foreground (AppDelegate)
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, ...) {
|
||||
// Trigger rollover immediately
|
||||
await handleNotificationRollover(...)
|
||||
}
|
||||
|
||||
// Detection Mechanism 2: Background (Recovery Manager)
|
||||
func performColdStartRecovery() async {
|
||||
// Check for delivered notifications
|
||||
await checkAndProcessDeliveredNotifications()
|
||||
}
|
||||
```
|
||||
|
||||
**Decision**: ✅ **Rely on existing recovery + AppDelegate, no dedicated background task**
|
||||
|
||||
---
|
||||
|
||||
## Question 4: Error Handling
|
||||
|
||||
**Question**: How should we handle rollover failures? Retry? Log? User notification?
|
||||
|
||||
### Current State Analysis
|
||||
|
||||
- **Android Pattern**: Logs errors, continues execution (non-fatal)
|
||||
- **iOS Recovery Manager**: Catches all errors, logs, continues (non-fatal)
|
||||
- **User Experience**: Failures should be silent (background operation)
|
||||
|
||||
### Recommendation: **Log + Continue (Non-Fatal)**
|
||||
|
||||
**Rationale**:
|
||||
1. **Background Operation**: Rollover is background, shouldn't interrupt user
|
||||
2. **Recovery Available**: Recovery manager will catch missed rollovers on next app launch
|
||||
3. **Consistency**: Matches Android and existing iOS recovery patterns
|
||||
4. **User Experience**: Silent failures, recovery on next launch
|
||||
|
||||
### Implementation Approach
|
||||
|
||||
**Error Handling Strategy**:
|
||||
1. **Log Errors**: Comprehensive logging for debugging
|
||||
2. **Continue Execution**: Don't crash or interrupt app
|
||||
3. **No Retry**: Let recovery manager handle on next launch
|
||||
4. **No User Notification**: Background operation, silent failure
|
||||
5. **History Recording**: Record failures in history (if history implemented)
|
||||
|
||||
### Code Pattern
|
||||
|
||||
```swift
|
||||
func scheduleNextNotification(...) async -> Bool {
|
||||
do {
|
||||
// Rollover logic
|
||||
return true
|
||||
} catch {
|
||||
print("\(Self.TAG): RESCHEDULE_ERR id=\(content.id) err=\(error.localizedDescription)")
|
||||
// Log error but don't throw - let recovery handle on next launch
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// In recovery manager:
|
||||
if !scheduled {
|
||||
print("\(Self.TAG): Failed to roll over delivered notification id=\(notificationId)")
|
||||
// Recovery will retry on next app launch
|
||||
}
|
||||
```
|
||||
|
||||
**Decision**: ✅ **Log + Continue (non-fatal), no retry, no user notification**
|
||||
|
||||
---
|
||||
|
||||
## Question 5: Performance
|
||||
|
||||
**Question**: Should we batch rollover operations or process individually?
|
||||
|
||||
### Current State Analysis
|
||||
|
||||
- **Android Pattern**: Processes individually (one notification at a time)
|
||||
- **iOS Recovery**: Processes notifications individually
|
||||
- **Volume**: Typically 1-2 notifications per day (low volume)
|
||||
|
||||
### Recommendation: **Process Individually**
|
||||
|
||||
**Rationale**:
|
||||
1. **Low Volume**: Typically 1 notification per day, batching unnecessary
|
||||
2. **Simplicity**: Individual processing is simpler and easier to debug
|
||||
3. **Error Isolation**: Individual processing isolates failures
|
||||
4. **Consistency**: Matches Android and existing iOS patterns
|
||||
|
||||
### Implementation Approach
|
||||
|
||||
**Individual Processing**:
|
||||
- Process each notification rollover separately
|
||||
- Each rollover is independent operation
|
||||
- Failures in one don't affect others
|
||||
- Easier to log and debug
|
||||
|
||||
**Future Optimization** (if needed):
|
||||
- If volume increases, consider batching
|
||||
- Current volume doesn't justify batching complexity
|
||||
|
||||
### Code Pattern
|
||||
|
||||
```swift
|
||||
// Process individually (current approach)
|
||||
for notification in deliveredNotifications {
|
||||
await scheduler.scheduleNextNotification(notification, ...)
|
||||
}
|
||||
|
||||
// Batching would look like:
|
||||
// await scheduler.scheduleNextNotificationsBatch(notifications, ...)
|
||||
// But not needed for current volume
|
||||
```
|
||||
|
||||
**Decision**: ✅ **Process individually (current volume doesn't justify batching)**
|
||||
|
||||
---
|
||||
|
||||
## Question 6: Testing
|
||||
|
||||
**Question**: Do we need automated tests for rollover, or is manual testing sufficient for Phase 1?
|
||||
|
||||
### Current State Analysis
|
||||
|
||||
- **Existing Tests**: iOS has unit tests for recovery manager
|
||||
- **Test Coverage**: Some components have tests, others don't
|
||||
- **Phase 1 Scope**: Core rollover functionality
|
||||
|
||||
### Recommendation: **Manual Testing for Phase 1, Automated Tests for Phase 2**
|
||||
|
||||
**Rationale**:
|
||||
1. **Phase 1 Focus**: Core functionality, manual testing sufficient
|
||||
2. **Complexity**: Rollover involves system notifications (hard to test automatically)
|
||||
3. **Time Investment**: Automated tests take time, manual testing faster for Phase 1
|
||||
4. **Phase 2**: Add automated tests when edge cases are implemented
|
||||
|
||||
### Implementation Approach
|
||||
|
||||
**Phase 1 Testing**:
|
||||
- Manual testing checklist
|
||||
- Test scenarios: foreground delivery, background delivery, duplicates
|
||||
- Real device testing (simulator may not handle notifications correctly)
|
||||
|
||||
**Phase 2 Testing**:
|
||||
- Unit tests for time calculations (DST, timezone)
|
||||
- Integration tests for rollover flow
|
||||
- Edge case tests (time changes, timezone changes)
|
||||
|
||||
### Test Checklist (Phase 1)
|
||||
|
||||
1. ✅ **Foreground Delivery**: App running, notification fires → rollover triggers
|
||||
2. ✅ **Background Delivery**: App not running, notification fires → rollover on launch
|
||||
3. ✅ **Duplicate Prevention**: Multiple rollover attempts → only one scheduled
|
||||
4. ✅ **DST Transition**: Schedule on DST day → correct time calculation
|
||||
5. ✅ **Error Handling**: Simulate failure → graceful degradation
|
||||
|
||||
**Decision**: ✅ **Manual testing for Phase 1, automated tests for Phase 2**
|
||||
|
||||
---
|
||||
|
||||
## Summary of Decisions
|
||||
|
||||
| Question | Decision | Rationale |
|
||||
|----------|----------|-----------|
|
||||
| **Fetcher Integration** | Defer to Phase 2, optional parameter | Prefetch is separate concern, Phase 1 focuses on core rollover |
|
||||
| **AppDelegate Access** | Notification Center pattern | Decoupling, flexibility, reliability |
|
||||
| **Background Task** | Rely on existing recovery | iOS limitations, existing infrastructure sufficient |
|
||||
| **Error Handling** | Log + Continue (non-fatal) | Background operation, recovery handles failures |
|
||||
| **Performance** | Process individually | Low volume, simplicity, consistency |
|
||||
| **Testing** | Manual for Phase 1, automated for Phase 2 | Phase 1 scope, complexity, time investment |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Impact
|
||||
|
||||
### Changes to Review Document
|
||||
|
||||
Based on these decisions, the review document should be updated:
|
||||
|
||||
1. **Fetcher Parameter**: Make optional, add Phase 2 TODOs
|
||||
2. **AppDelegate Pattern**: Use Notification Center instead of Capacitor bridge
|
||||
3. **Background Task**: Remove dedicated background task, rely on recovery
|
||||
4. **Error Handling**: Add comprehensive logging, non-fatal errors
|
||||
5. **Performance**: Individual processing (no batching)
|
||||
6. **Testing**: Manual testing checklist for Phase 1
|
||||
|
||||
### Next Steps
|
||||
|
||||
1. ✅ **Decisions Made** (This document)
|
||||
2. **Update Review Document** with decisions
|
||||
3. **Update Implementation Plan** with specific patterns
|
||||
4. **Begin Phase 1 Implementation**
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Review Document: `docs/ios-rollover-implementation-review.md`
|
||||
- Edge Case Plan: `docs/ios-rollover-edge-case-plan.md`
|
||||
- Android Implementation: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java`
|
||||
- iOS Recovery Manager: `ios/Plugin/DailyNotificationReactivationManager.swift`
|
||||
- iOS Background Tasks: `ios/Plugin/DailyNotificationBackgroundTaskManager.swift`
|
||||
574
doc/platform/ios/TROUBLESHOOTING.md
Normal file
574
doc/platform/ios/TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,574 @@
|
||||
# iOS Troubleshooting Guide
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-12-08
|
||||
**Status**: 🎯 **ACTIVE** - iOS Troubleshooting Reference
|
||||
**Version**: 1.0.0
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide provides solutions to common iOS-specific issues when using the Daily Notification Plugin. It covers debugging techniques, common problems, and their solutions.
|
||||
|
||||
**Reference**:
|
||||
- [iOS Implementation Directive](./ios-implementation-directive.md) - Implementation details
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - iOS OS behaviors
|
||||
- [iOS Logging Guide](../doc/test-app-ios/IOS_LOGGING_GUIDE.md) - How to view logs
|
||||
|
||||
---
|
||||
|
||||
## 1. Common Issues
|
||||
|
||||
### 1.1 Notifications Not Firing
|
||||
|
||||
**Symptoms:**
|
||||
- Notifications scheduled but don't appear
|
||||
- No notification at scheduled time
|
||||
- Notifications work in simulator but not on device
|
||||
|
||||
**Diagnosis Steps:**
|
||||
|
||||
1. **Check Notification Permission:**
|
||||
```swift
|
||||
// In app code
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
print("Authorization status: \(settings.authorizationStatus)")
|
||||
}
|
||||
```
|
||||
|
||||
Or check in Xcode Console:
|
||||
```
|
||||
DNP-PLUGIN: Notification permission status: authorized
|
||||
```
|
||||
|
||||
2. **Check Pending Notifications:**
|
||||
```swift
|
||||
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
|
||||
print("Pending notifications: \(requests.count)")
|
||||
}
|
||||
```
|
||||
|
||||
3. **Check Background App Refresh:**
|
||||
- Settings → [Your App] → Background App Refresh
|
||||
- Must be enabled for background tasks
|
||||
|
||||
4. **Check Notification Limit:**
|
||||
- iOS limits to 64 pending notifications
|
||||
- Check if limit is exceeded
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Permission Denied:**
|
||||
- Request permission: `DailyNotification.requestNotificationPermission()`
|
||||
- Guide user to Settings → [Your App] → Notifications
|
||||
|
||||
- **Background App Refresh Disabled:**
|
||||
- Guide user to enable: Settings → [Your App] → Background App Refresh
|
||||
- Or use: `DailyNotification.openBackgroundAppRefreshSettings()`
|
||||
|
||||
- **Notification Limit Exceeded:**
|
||||
- Reduce number of scheduled notifications
|
||||
- Implement notification cleanup logic
|
||||
- Check rolling window implementation
|
||||
|
||||
- **Simulator vs Device:**
|
||||
- Simulator may not fire notifications reliably
|
||||
- Test on physical device for accurate behavior
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Background Tasks Not Executing
|
||||
|
||||
**Symptoms:**
|
||||
- Prefetch tasks don't run
|
||||
- Background fetch never executes
|
||||
- BGTaskScheduler tasks not firing
|
||||
|
||||
**Diagnosis Steps:**
|
||||
|
||||
1. **Check BGTaskScheduler Registration:**
|
||||
```swift
|
||||
// Verify registration in AppDelegate
|
||||
BGTaskScheduler.shared.register(forTaskWithIdentifier: "org.timesafari.dailynotification.fetch", using: nil) { task in
|
||||
// Handler should be registered
|
||||
}
|
||||
```
|
||||
|
||||
2. **Check Info.plist:**
|
||||
```xml
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>org.timesafari.dailynotification.fetch</string>
|
||||
</array>
|
||||
```
|
||||
|
||||
3. **Check Background App Refresh:**
|
||||
- Must be enabled in Settings
|
||||
- System-controlled timing (not guaranteed)
|
||||
|
||||
4. **Check Logs:**
|
||||
```
|
||||
DNP-FETCH-START: Background fetch task started
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Not Registered:**
|
||||
- Verify registration in `AppDelegate.application(_:didFinishLaunchingWithOptions:)`
|
||||
- Check Info.plist has correct identifiers
|
||||
|
||||
- **Background App Refresh Disabled:**
|
||||
- User must enable in Settings
|
||||
- Cannot be programmatically enabled
|
||||
|
||||
- **System Not Executing:**
|
||||
- BGTaskScheduler is system-controlled
|
||||
- Execution timing is not guaranteed
|
||||
- System may defer or skip tasks
|
||||
- Use for prefetching only, not critical scheduling
|
||||
|
||||
- **Simulator Limitations:**
|
||||
- Background tasks may not execute in simulator
|
||||
- Test on physical device
|
||||
|
||||
---
|
||||
|
||||
### 1.3 Notifications Disappear After App Termination
|
||||
|
||||
**Symptoms:**
|
||||
- Notifications scheduled but disappear when app is terminated
|
||||
- Notifications don't persist across app restarts
|
||||
|
||||
**Diagnosis:**
|
||||
|
||||
**This should NOT happen on iOS** - notifications persist automatically (OS-guaranteed).
|
||||
|
||||
**If it happens, check:**
|
||||
|
||||
1. **Notification Trigger Type:**
|
||||
- Calendar/time triggers persist ✅
|
||||
- Location triggers do NOT persist ❌
|
||||
|
||||
2. **Notification Content:**
|
||||
- Ensure notification has valid content
|
||||
- Check for invalid trigger dates
|
||||
|
||||
3. **System Storage:**
|
||||
- iOS may clear notifications if device storage is full
|
||||
- Check available storage
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Use Calendar Triggers:**
|
||||
```swift
|
||||
// ✅ Persists across reboot
|
||||
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
|
||||
|
||||
// ❌ Does NOT persist
|
||||
let trigger = UNLocationNotificationTrigger(region: region, repeats: true)
|
||||
```
|
||||
|
||||
- **Check Device Storage:**
|
||||
- Free up storage if device is full
|
||||
- iOS may clear notifications when storage is critical
|
||||
|
||||
---
|
||||
|
||||
### 1.4 Recovery Not Working
|
||||
|
||||
**Symptoms:**
|
||||
- Missed notifications not detected on app launch
|
||||
- Future notifications not verified/rescheduled
|
||||
- No recovery activity in logs
|
||||
|
||||
**Diagnosis:**
|
||||
|
||||
**Recovery features are NOT yet implemented** (as of 2025-12-08).
|
||||
|
||||
**Expected Behavior (Once Implemented):**
|
||||
|
||||
1. **Check Logs for Recovery:**
|
||||
```
|
||||
DNP-REACTIVATION: Starting app launch recovery
|
||||
DNP-REACTIVATION: Detected scenario: coldStart
|
||||
DNP-REACTIVATION: Missed notifications detected: 2
|
||||
DNP-REACTIVATION: Future notifications verified: 1
|
||||
```
|
||||
|
||||
2. **Verify Recovery Logic:**
|
||||
- Should run in `DailyNotificationPlugin.load()`
|
||||
- Should detect missed notifications
|
||||
- Should verify future notifications
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Implementation Pending:**
|
||||
- See [iOS Implementation Directive Phase 1](./ios-implementation-directive-phase1.md)
|
||||
- Recovery features need to be implemented
|
||||
|
||||
- **Manual Workaround:**
|
||||
- Check pending notifications manually
|
||||
- Reschedule if needed
|
||||
|
||||
---
|
||||
|
||||
### 1.5 Database/Storage Issues
|
||||
|
||||
**Symptoms:**
|
||||
- Database errors in logs
|
||||
- Data not persisting
|
||||
- Core Data errors
|
||||
|
||||
**Diagnosis Steps:**
|
||||
|
||||
1. **Check Database Path:**
|
||||
```swift
|
||||
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let dbPath = documentsPath.appendingPathComponent("daily_notifications.db")
|
||||
print("Database path: \(dbPath.path)")
|
||||
```
|
||||
|
||||
2. **Check Core Data Stack:**
|
||||
- Verify `NSPersistentContainer` initialization
|
||||
- Check for migration errors
|
||||
|
||||
3. **Check Logs:**
|
||||
```
|
||||
DNP-STORAGE: Database opened successfully
|
||||
DNP-STORAGE: Error opening database: [error]
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Database Path Issues:**
|
||||
- Verify app has write permissions
|
||||
- Check Documents directory is accessible
|
||||
- Ensure path is correct
|
||||
|
||||
- **Core Data Errors:**
|
||||
- Check Core Data model version
|
||||
- Verify migration policies
|
||||
- Check for schema mismatches
|
||||
|
||||
- **Storage Full:**
|
||||
- Free up device storage
|
||||
- iOS may clear app data if storage is critical
|
||||
|
||||
---
|
||||
|
||||
### 1.6 Permission Issues
|
||||
|
||||
**Symptoms:**
|
||||
- Permission requests fail
|
||||
- Permission status incorrect
|
||||
- Cannot schedule notifications
|
||||
|
||||
**Diagnosis:**
|
||||
|
||||
1. **Check Current Status:**
|
||||
```swift
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
switch settings.authorizationStatus {
|
||||
case .authorized: // ✅ Can schedule
|
||||
case .denied: // ❌ User denied
|
||||
case .notDetermined: // ⚠️ Not requested yet
|
||||
case .provisional: // ⚠️ Provisional (iOS 12+)
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Check Logs:**
|
||||
```
|
||||
DNP-PLUGIN: Notification permission status: denied
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Permission Denied:**
|
||||
- Cannot request again programmatically
|
||||
- Guide user to Settings → [Your App] → Notifications
|
||||
- Use: `DailyNotification.openNotificationSettings()`
|
||||
|
||||
- **Not Determined:**
|
||||
- Request permission: `DailyNotification.requestNotificationPermission()`
|
||||
- Show explanation before requesting
|
||||
|
||||
- **Provisional:**
|
||||
- iOS 12+ feature
|
||||
- Notifications delivered quietly
|
||||
- User can upgrade to full permission
|
||||
|
||||
---
|
||||
|
||||
## 2. Debugging Techniques
|
||||
|
||||
### 2.1 Viewing Logs
|
||||
|
||||
**Xcode Console (Recommended):**
|
||||
1. Run app in Xcode (Cmd+R)
|
||||
2. Open Debug Area (Cmd+Shift+Y)
|
||||
3. Filter by: `DNP-` or `DailyNotification`
|
||||
|
||||
**Console.app:**
|
||||
1. Open Console.app
|
||||
2. Select device/simulator
|
||||
3. Filter by process: `ios-test-app`
|
||||
|
||||
**Command Line:**
|
||||
```bash
|
||||
# Simulator logs
|
||||
xcrun simctl spawn <device-id> log stream --level=debug --predicate 'processImagePath contains "ios-test-app"'
|
||||
|
||||
# Device logs (requires device connected)
|
||||
xcrun devicectl device process monitor --device <device-id> --filter "ios-test-app"
|
||||
```
|
||||
|
||||
### 2.2 Checking Pending Notifications
|
||||
|
||||
**Via Plugin Method:**
|
||||
```typescript
|
||||
const result = await DailyNotification.getPendingNotifications();
|
||||
console.log(`Pending: ${result.count}`);
|
||||
```
|
||||
|
||||
**Via Swift Code:**
|
||||
```swift
|
||||
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
|
||||
print("Pending notifications: \(requests.count)")
|
||||
for request in requests {
|
||||
print(" - \(request.identifier): \(request.content.title)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Checking Background Task Status
|
||||
|
||||
**Via Plugin Method:**
|
||||
```typescript
|
||||
const status = await DailyNotification.getBackgroundTaskStatus();
|
||||
console.log(`Fetch task registered: ${status.fetchTaskRegistered}`);
|
||||
console.log(`Background refresh enabled: ${status.backgroundRefreshEnabled}`);
|
||||
```
|
||||
|
||||
**Via Swift Code:**
|
||||
```swift
|
||||
// Check registration
|
||||
let registered = BGTaskScheduler.shared.registeredTaskIdentifiers
|
||||
print("Registered tasks: \(registered)")
|
||||
|
||||
// Check Background App Refresh (requires entitlement)
|
||||
// Cannot check programmatically - must guide user to Settings
|
||||
```
|
||||
|
||||
### 2.4 Simulating Background Tasks (Simulator Only)
|
||||
|
||||
**LLDB Command in Xcode:**
|
||||
```lldb
|
||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"org.timesafari.dailynotification.fetch"]
|
||||
```
|
||||
|
||||
**Note:** This only works in simulator, not on physical devices.
|
||||
|
||||
---
|
||||
|
||||
## 3. Platform-Specific Considerations
|
||||
|
||||
### 3.1 Simulator vs Device
|
||||
|
||||
**Simulator Limitations:**
|
||||
- Background tasks may not execute reliably
|
||||
- Notifications may not fire at exact time
|
||||
- Some features require physical device
|
||||
|
||||
**Device Testing:**
|
||||
- More accurate behavior
|
||||
- Background tasks execute (system-controlled)
|
||||
- Notifications fire reliably
|
||||
|
||||
**Recommendation:** Test critical features on physical device.
|
||||
|
||||
### 3.2 iOS Version Differences
|
||||
|
||||
**iOS 12+:**
|
||||
- Provisional notification authorization
|
||||
- Background task improvements
|
||||
|
||||
**iOS 13+:**
|
||||
- State actor support (concurrency)
|
||||
- Improved background execution
|
||||
|
||||
**iOS 14+:**
|
||||
- Notification interruption levels
|
||||
- Focus modes (may affect notifications)
|
||||
|
||||
**iOS 15+:**
|
||||
- Notification summary
|
||||
- Focus mode integration
|
||||
|
||||
### 3.3 Background Execution Limits
|
||||
|
||||
**iOS Constraints:**
|
||||
- BGTaskScheduler is system-controlled
|
||||
- Execution timing not guaranteed
|
||||
- Minimum intervals between tasks (hours)
|
||||
- Tasks may be deferred or skipped
|
||||
|
||||
**Workaround:**
|
||||
- Use BGTaskScheduler for prefetching only
|
||||
- Don't rely on it for critical scheduling
|
||||
- Use UNUserNotificationCenter for notifications (more reliable)
|
||||
|
||||
---
|
||||
|
||||
## 4. Error Codes
|
||||
|
||||
### 4.1 Common Error Codes
|
||||
|
||||
| Error Code | Description | Solution |
|
||||
| ---------- | ----------- | -------- |
|
||||
| `NOTIFICATION_PERMISSION_DENIED` | User denied notification permission | Guide user to Settings |
|
||||
| `BACKGROUND_REFRESH_DISABLED` | Background App Refresh disabled | Guide user to enable in Settings |
|
||||
| `PENDING_NOTIFICATION_LIMIT_EXCEEDED` | Exceeded 64 notification limit | Reduce scheduled notifications |
|
||||
| `BG_TASK_NOT_REGISTERED` | Background task not registered | Check Info.plist and AppDelegate |
|
||||
| `BG_TASK_EXECUTION_FAILED` | Background task execution failed | Check logs for specific error |
|
||||
|
||||
### 4.2 Checking Error Details
|
||||
|
||||
**Via Logs:**
|
||||
```
|
||||
DNP-ERROR: [Error Code] [Error Message]
|
||||
DNP-ERROR: NOTIFICATION_PERMISSION_DENIED: User denied notification permission
|
||||
```
|
||||
|
||||
**Via Plugin:**
|
||||
```typescript
|
||||
try {
|
||||
await DailyNotification.scheduleDailyNotification({...});
|
||||
} catch (error) {
|
||||
console.error('Error code:', error.code);
|
||||
console.error('Error message:', error.message);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Performance Issues
|
||||
|
||||
### 5.1 Slow Notification Scheduling
|
||||
|
||||
**Symptoms:**
|
||||
- Scheduling takes too long
|
||||
- App freezes during scheduling
|
||||
|
||||
**Solutions:**
|
||||
- Schedule notifications asynchronously
|
||||
- Batch operations when possible
|
||||
- Use background queue for heavy operations
|
||||
|
||||
### 5.2 High Memory Usage
|
||||
|
||||
**Symptoms:**
|
||||
- App memory usage high
|
||||
- Memory warnings in logs
|
||||
|
||||
**Solutions:**
|
||||
- Implement notification cleanup
|
||||
- Limit cached notifications
|
||||
- Use efficient data structures
|
||||
|
||||
### 5.3 Battery Drain
|
||||
|
||||
**Symptoms:**
|
||||
- Battery drains quickly
|
||||
- Background activity high
|
||||
|
||||
**Solutions:**
|
||||
- Limit background task frequency
|
||||
- Optimize prefetch operations
|
||||
- Use efficient scheduling algorithms
|
||||
|
||||
---
|
||||
|
||||
## 6. Getting Help
|
||||
|
||||
### 6.1 Log Collection
|
||||
|
||||
**Collect Logs:**
|
||||
1. Reproduce the issue
|
||||
2. Collect logs from Xcode Console or Console.app
|
||||
3. Filter by `DNP-` prefix
|
||||
4. Include relevant error messages
|
||||
|
||||
**Log Format:**
|
||||
```
|
||||
DNP-PLUGIN: [Message]
|
||||
DNP-ERROR: [Error Code] [Error Message]
|
||||
DNP-REACTIVATION: [Recovery Activity]
|
||||
```
|
||||
|
||||
### 6.2 Issue Reporting
|
||||
|
||||
**Include:**
|
||||
- iOS version
|
||||
- Device model (or simulator)
|
||||
- Plugin version
|
||||
- Steps to reproduce
|
||||
- Relevant logs
|
||||
- Expected vs actual behavior
|
||||
|
||||
### 6.3 Documentation References
|
||||
|
||||
- [iOS Implementation Directive](./ios-implementation-directive.md)
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md)
|
||||
- [API Reference](../API.md)
|
||||
- [iOS Logging Guide](../doc/test-app-ios/IOS_LOGGING_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
## 7. Quick Reference
|
||||
|
||||
### 7.1 Common Commands
|
||||
|
||||
**Check Pending Notifications:**
|
||||
```bash
|
||||
# Via plugin method (recommended)
|
||||
# Or check logs for scheduling activity
|
||||
```
|
||||
|
||||
**View Logs:**
|
||||
```bash
|
||||
# Xcode Console (Cmd+Shift+Y)
|
||||
# Filter: DNP-
|
||||
|
||||
# Console.app
|
||||
# Filter: ios-test-app
|
||||
```
|
||||
|
||||
**Check Permissions:**
|
||||
```typescript
|
||||
const status = await DailyNotification.getNotificationPermissionStatus();
|
||||
```
|
||||
|
||||
**Open Settings:**
|
||||
```typescript
|
||||
await DailyNotification.openNotificationSettings();
|
||||
await DailyNotification.openBackgroundAppRefreshSettings();
|
||||
```
|
||||
|
||||
### 7.2 Checklist
|
||||
|
||||
**Before Reporting Issue:**
|
||||
- [ ] Checked notification permissions
|
||||
- [ ] Verified Background App Refresh is enabled
|
||||
- [ ] Checked pending notification count (< 64)
|
||||
- [ ] Reviewed logs for errors
|
||||
- [ ] Tested on physical device (not just simulator)
|
||||
- [ ] Verified iOS version compatibility
|
||||
- [ ] Checked device storage availability
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-08
|
||||
**Next Review**: After Phase 1 implementation
|
||||
|
||||
Reference in New Issue
Block a user