50 KiB
DailyNotification Plugin Architecture
Author: Matthew Raymer
Version: 1.0.0
Date: October 20, 2025
Status: 🎯 ACTIVE - Production-ready architecture
Table of Contents
- Overview
- Architecture Principles
- Core Components
- Data Architecture
- Storage Implementation
- Plugin Integration
- Security Architecture
- Performance Architecture
- Migration Strategy
- Testing Architecture
- Deployment Architecture
- Monitoring & Analytics
- Future Architecture
Overview
The DailyNotification plugin is a Capacitor-based cross-platform notification system designed for TimeSafari applications. It provides enterprise-grade notification management with offline-first capabilities, exact-time scheduling, and comprehensive analytics.
Key Capabilities
- Exact-Time Scheduling: DST-safe notifications with Doze mode compatibility
- Offline-First Design: Works without network connectivity
- Enterprise Data Management: Room database with encryption and retention policies
- Cross-Platform Support: Android (native) and iOS (planned)
- Comprehensive Analytics: Delivery tracking, user interaction analysis, and performance metrics
- Schema Validation: Runtime validation with Zod for data integrity
- Permission Management: Graceful handling of Android 13+ permissions
Architecture Goals
- Reliability: 99.9% notification delivery success rate
- Performance: Sub-100ms response times for critical operations
- Scalability: Support for 10,000+ notifications per user
- Security: End-to-end encryption for sensitive data
- Maintainability: Clean separation of concerns and comprehensive testing
Architecture Principles
1. Plugin-First Design
The plugin operates as a self-contained unit with its own:
- Database schema and storage
- Configuration management
- Analytics and reporting
- Security and encryption
- Lifecycle management
2. Offline-First Architecture
All core functionality works without network connectivity:
- Local notification scheduling
- Cached content delivery
- Offline analytics collection
- Local configuration management
3. Data Integrity First
Schema validation at every boundary:
- TypeScript/Zod validation for JavaScript inputs
- Room database constraints for native data
- Encryption for sensitive information
- Retention policies for data lifecycle
4. Performance by Design
Optimized for mobile constraints:
- Background thread execution
- Efficient database queries with proper indexing
- Work deduplication and idempotence
- Battery-aware operations
5. Security by Default
Defense in depth:
- Encryption at rest for sensitive data
- Secure key management
- Permission-based access control
- Audit logging for all operations
Core Components
Plugin Architecture Overview
graph TB
subgraph "JavaScript Layer"
A[Vue3 App] --> B[NotificationService]
B --> C[ValidationService]
B --> D[PermissionManager]
B --> E[Capacitor Bridge]
end
subgraph "Native Android Layer"
E --> F[DailyNotificationPlugin]
F --> G[Scheduler]
F --> H[StorageRoom]
F --> I[Worker]
F --> J[Receiver]
end
subgraph "Data Layer"
H --> K[Room Database]
K --> L[Content Entity]
K --> M[Delivery Entity]
K --> N[Config Entity]
end
subgraph "System Integration"
G --> O[AlarmManager]
I --> P[WorkManager]
J --> Q[NotificationManager]
end
Component Responsibilities
JavaScript Layer
Component | Responsibility | Key Features |
---|---|---|
NotificationService | High-level notification operations | Schedule, cancel, query notifications |
ValidationService | Runtime data validation | Zod schemas, type safety, error handling |
PermissionManager | Permission handling and UX | Android 13+ permissions, user education |
Capacitor Bridge | Native communication | Method calls, event handling, error propagation |
Native Android Layer
Component | Responsibility | Key Features |
---|---|---|
DailyNotificationPlugin | Main plugin interface | Capacitor integration, method routing |
DailyNotificationScheduler | Time-based scheduling | DST-safe calculations, exact alarms |
DailyNotificationStorageRoom | Data persistence | Room database, encryption, analytics |
DailyNotificationWorker | Background processing | Work deduplication, retry logic |
DailyNotificationReceiver | System event handling | Alarm triggers, notification display |
Data Architecture
Database Schema Design
The plugin uses Room database with three main entities:
1. NotificationContentEntity
Purpose: Store notification content and metadata
@Entity(tableName = "notification_content")
public class NotificationContentEntity {
@PrimaryKey String id; // Unique notification ID
String pluginVersion; // Plugin version for migration
String timesafariDid; // TimeSafari user identifier
String notificationType; // Type classification
String title; // Notification title
String body; // Notification body
long scheduledTime; // When to deliver
String timezone; // User timezone
int priority; // Notification priority
boolean vibrationEnabled; // Vibration setting
boolean soundEnabled; // Sound setting
String mediaUrl; // Media attachment URL
String encryptedContent; // Encrypted sensitive data
String encryptionKeyId; // Encryption key identifier
long createdAt; // Creation timestamp
long updatedAt; // Last update timestamp
long ttlSeconds; // Time-to-live
String deliveryStatus; // Current delivery status
int deliveryAttempts; // Number of delivery attempts
long lastDeliveryAttempt; // Last attempt timestamp
int userInteractionCount; // User interaction count
long lastUserInteraction; // Last interaction timestamp
String metadata; // Additional metadata
}
2. NotificationDeliveryEntity
Purpose: Track delivery events and analytics
@Entity(tableName = "notification_delivery")
public class NotificationDeliveryEntity {
@PrimaryKey String id; // Unique delivery ID
String notificationId; // Reference to notification
String timesafariDid; // TimeSafari user identifier
long deliveryTimestamp; // When delivery occurred
String deliveryStatus; // Success/failure status
String deliveryMethod; // How it was delivered
int deliveryAttemptNumber; // Attempt sequence number
long deliveryDurationMs; // How long delivery took
String userInteractionType; // Type of user interaction
long userInteractionTimestamp; // When interaction occurred
long userInteractionDurationMs; // Interaction duration
String errorCode; // Error classification
String errorMessage; // Detailed error message
String deviceInfo; // Device context
String networkInfo; // Network context
int batteryLevel; // Battery level at delivery
boolean dozeModeActive; // Doze mode status
boolean exactAlarmPermission; // Exact alarm permission
boolean notificationPermission; // Notification permission
String metadata; // Additional context
}
3. NotificationConfigEntity
Purpose: Store configuration and user preferences
@Entity(tableName = "notification_config")
public class NotificationConfigEntity {
@PrimaryKey String id; // Unique config ID
String timesafariDid; // TimeSafari user identifier
String configType; // Configuration category
String configKey; // Configuration key
String configValue; // Configuration value
String configDataType; // Data type (boolean, int, string, json)
boolean isEncrypted; // Encryption flag
String encryptionKeyId; // Encryption key identifier
long createdAt; // Creation timestamp
long updatedAt; // Last update timestamp
long ttlSeconds; // Time-to-live
boolean isActive; // Active status
String metadata; // Additional metadata
}
Database Relationships
erDiagram
NotificationContentEntity ||--o{ NotificationDeliveryEntity : "tracks"
NotificationContentEntity ||--o{ NotificationConfigEntity : "configured by"
NotificationContentEntity {
string id PK
string timesafariDid
string notificationType
long scheduledTime
string deliveryStatus
}
NotificationDeliveryEntity {
string id PK
string notificationId FK
string timesafariDid
string deliveryStatus
long deliveryTimestamp
}
NotificationConfigEntity {
string id PK
string timesafariDid
string configType
string configKey
string configValue
}
Indexing Strategy
Performance-optimized indexes for common query patterns:
-- NotificationContentEntity indexes
CREATE INDEX idx_notification_content_timesafari_did ON notification_content(timesafari_did);
CREATE INDEX idx_notification_content_type ON notification_content(notification_type);
CREATE INDEX idx_notification_content_scheduled_time ON notification_content(scheduled_time);
CREATE INDEX idx_notification_content_created_at ON notification_content(created_at);
CREATE INDEX idx_notification_content_plugin_version ON notification_content(plugin_version);
-- NotificationDeliveryEntity indexes
CREATE INDEX idx_notification_delivery_notification_id ON notification_delivery(notification_id);
CREATE INDEX idx_notification_delivery_timestamp ON notification_delivery(delivery_timestamp);
CREATE INDEX idx_notification_delivery_status ON notification_delivery(delivery_status);
CREATE INDEX idx_notification_delivery_user_interaction ON notification_delivery(user_interaction_type);
CREATE INDEX idx_notification_delivery_timesafari_did ON notification_delivery(timesafari_did);
-- NotificationConfigEntity indexes
CREATE INDEX idx_notification_config_timesafari_did ON notification_config(timesafari_did);
CREATE INDEX idx_notification_config_type ON notification_config(config_type);
CREATE INDEX idx_notification_config_updated_at ON notification_config(updated_at);
Storage Implementation
Room Database Architecture
Database Class
@Database(
entities = {
NotificationContentEntity.class,
NotificationDeliveryEntity.class,
NotificationConfigEntity.class
},
version = 1,
exportSchema = false
)
public abstract class DailyNotificationDatabase extends RoomDatabase {
private static final String DATABASE_NAME = "daily_notification_plugin.db";
private static volatile DailyNotificationDatabase INSTANCE;
public abstract NotificationContentDao notificationContentDao();
public abstract NotificationDeliveryDao notificationDeliveryDao();
public abstract NotificationConfigDao notificationConfigDao();
public static DailyNotificationDatabase getInstance(Context context) {
return Room.databaseBuilder(
context.getApplicationContext(),
DailyNotificationDatabase.class,
DATABASE_NAME
).build();
}
}
DAO Pattern Implementation
NotificationContentDao - Comprehensive CRUD operations:
@Dao
public interface NotificationContentDao {
// Basic CRUD
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertNotification(NotificationContentEntity notification);
@Query("SELECT * FROM notification_content WHERE id = :id")
NotificationContentEntity getNotificationById(String id);
@Update
void updateNotification(NotificationContentEntity notification);
@Query("DELETE FROM notification_content WHERE id = :id")
void deleteNotification(String id);
// Plugin-specific queries
@Query("SELECT * FROM notification_content WHERE timesafari_did = :timesafariDid ORDER BY scheduled_time ASC")
List<NotificationContentEntity> getNotificationsByTimeSafariDid(String timesafariDid);
@Query("SELECT * FROM notification_content WHERE scheduled_time <= :currentTime AND delivery_status != 'delivered' ORDER BY scheduled_time ASC")
List<NotificationContentEntity> getNotificationsReadyForDelivery(long currentTime);
// Analytics queries
@Query("SELECT COUNT(*) FROM notification_content WHERE notification_type = :notificationType")
int getNotificationCountByType(String notificationType);
// Cleanup operations
@Query("DELETE FROM notification_content WHERE (created_at + (ttl_seconds * 1000)) < :currentTime")
int deleteExpiredNotifications(long currentTime);
}
Storage Service Architecture
DailyNotificationStorageRoom
High-level storage service with async operations:
public class DailyNotificationStorageRoom {
private DailyNotificationDatabase database;
private ExecutorService executorService;
// Async notification operations
public CompletableFuture<Boolean> saveNotificationContent(NotificationContentEntity content) {
return CompletableFuture.supplyAsync(() -> {
content.pluginVersion = PLUGIN_VERSION;
content.touch();
contentDao.insertNotification(content);
return true;
}, executorService);
}
// Analytics operations
public CompletableFuture<DeliveryAnalytics> getDeliveryAnalytics(String timesafariDid) {
return CompletableFuture.supplyAsync(() -> {
List<NotificationDeliveryEntity> deliveries = deliveryDao.getDeliveriesByTimeSafariDid(timesafariDid);
// Calculate analytics...
return analytics;
}, executorService);
}
}
Data Migration Strategy
From SharedPreferences to Room
Migration process for existing data:
public class StorageMigration {
public void migrateFromSharedPreferences(Context context) {
SharedPreferences prefs = context.getSharedPreferences("DailyNotificationPrefs", Context.MODE_PRIVATE);
Map<String, ?> allData = prefs.getAll();
for (Map.Entry<String, ?> entry : allData.entrySet()) {
if (entry.getKey().startsWith("notification_")) {
// Convert SharedPreferences data to Room entity
NotificationContentEntity entity = convertToEntity(entry.getValue());
database.notificationContentDao().insertNotification(entity);
}
}
}
private NotificationContentEntity convertToEntity(Object value) {
// Conversion logic from SharedPreferences format to Room entity
// Handles data type conversion, field mapping, and validation
}
}
Plugin Integration
Capacitor Plugin Architecture
Plugin Registration
Automatic plugin discovery via Capacitor annotation processor:
@CapacitorPlugin(name = "DailyNotification")
public class DailyNotificationPlugin extends Plugin {
private DailyNotificationScheduler scheduler;
private DailyNotificationStorageRoom storage;
@Override
public void load() {
scheduler = new DailyNotificationScheduler(getContext());
storage = new DailyNotificationStorageRoom(getContext());
}
@PluginMethod
public void scheduleDailyNotification(PluginCall call) {
// Implementation with validation and error handling
}
}
Method Routing
Centralized method handling with validation:
@PluginMethod
public void scheduleDailyNotification(PluginCall call) {
try {
// Validate input with Zod schema
NotificationOptions options = NotificationValidationService.validateNotificationOptions(call.getData());
// Create notification entity
NotificationContentEntity entity = new NotificationContentEntity(
options.id, PLUGIN_VERSION, options.timesafariDid,
options.type, options.title, options.body,
options.scheduledTime, options.timezone
);
// Save to Room database
storage.saveNotificationContent(entity).thenAccept(success -> {
if (success) {
// Schedule with AlarmManager
scheduler.scheduleExactAlarm(entity);
call.resolve();
} else {
call.reject("Failed to save notification");
}
});
} catch (ValidationException e) {
call.reject("Invalid notification options: " + e.getMessage());
}
}
JavaScript Integration
Service Layer Architecture
High-level service for Vue3 applications:
export class NotificationService {
private plugin: ValidatedDailyNotificationPlugin;
private permissionManager: NotificationPermissionManager;
async scheduleNotification(options: NotificationOptions): Promise<boolean> {
// Validate permissions
const hasPermissions = await this.permissionManager.checkPermissions();
if (!hasPermissions) {
throw new Error('Notification permissions required');
}
// Schedule notification
const result = await this.plugin.scheduleDailyNotification(options);
return result.success;
}
async getNotificationStatus(): Promise<NotificationStatus> {
return await this.plugin.checkStatus();
}
}
Validation Integration
Schema validation at service boundaries:
export class NotificationValidationService {
static validateNotificationOptions(data: any): NotificationOptions {
const result = NotificationOptionsSchema.safeParse(data);
if (!result.success) {
throw new ValidationException('Invalid notification options', result.error);
}
return result.data;
}
static validateReminderOptions(data: any): ReminderOptions {
const result = ReminderOptionsSchema.safeParse(data);
if (!result.success) {
throw new ValidationException('Invalid reminder options', result.error);
}
return result.data;
}
}
Security Architecture
Encryption Strategy
At-Rest Encryption
Sensitive data encryption for notification content:
public class NotificationEncryption {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int KEY_LENGTH = 256;
public String encryptContent(String content, String keyId) {
try {
SecretKey key = getOrCreateKey(keyId);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encryptedBytes = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
byte[] iv = cipher.getIV();
// Combine IV + encrypted data
byte[] combined = new byte[iv.length + encryptedBytes.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(encryptedBytes, 0, combined, iv.length, encryptedBytes.length);
return Base64.encodeToString(combined, Base64.DEFAULT);
} catch (Exception e) {
throw new EncryptionException("Failed to encrypt content", e);
}
}
}
Key Management
Secure key storage using Android Keystore:
public class NotificationKeyManager {
private static final String KEY_ALIAS = "DailyNotificationKey";
private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
public SecretKey getOrCreateKey(String keyId) {
try {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
if (keyStore.containsAlias(KEY_ALIAS)) {
return (SecretKey) keyStore.getKey(KEY_ALIAS, null);
} else {
return createNewKey();
}
} catch (Exception e) {
throw new KeyManagementException("Failed to get encryption key", e);
}
}
private SecretKey createNewKey() {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(KEY_LENGTH)
.build();
keyGenerator.init(keyGenParameterSpec);
return keyGenerator.generateKey();
} catch (Exception e) {
throw new KeyManagementException("Failed to create encryption key", e);
}
}
}
Permission Architecture
Android 13+ Permission Handling
Graceful permission management with user education:
export class NotificationPermissionManager {
async checkPermissions(): Promise<PermissionStatus> {
const platform = Capacitor.getPlatform();
if (platform === 'web') {
return { granted: false, reason: 'Web platform not supported' };
}
// Check POST_NOTIFICATIONS permission (Android 13+)
const hasNotificationPermission = await this.checkNotificationPermission();
// Check SCHEDULE_EXACT_ALARM permission (Android 12+)
const hasExactAlarmPermission = await this.checkExactAlarmPermission();
return {
granted: hasNotificationPermission && hasExactAlarmPermission,
notificationPermission: hasNotificationPermission,
exactAlarmPermission: hasExactAlarmPermission
};
}
async requestPermissions(): Promise<boolean> {
const status = await this.checkPermissions();
if (!status.notificationPermission) {
await this.requestNotificationPermission();
}
if (!status.exactAlarmPermission) {
await this.requestExactAlarmPermission();
}
return status.granted;
}
}
Permission UX Flow
User-friendly permission requests with education:
export class PermissionUXManager {
async showPermissionRationale(): Promise<void> {
const rationale = `
Daily notifications help you stay on track with your TimeSafari goals.
We need:
• Notification permission to show reminders
• Exact alarm permission for precise timing
You can change these settings later in your device settings.
`;
await this.showDialog({
title: 'Enable Notifications',
message: rationale,
buttons: [
{ text: 'Not Now', role: 'cancel' },
{ text: 'Enable', handler: () => this.requestPermissions() }
]
});
}
}
Performance Architecture
Background Processing
WorkManager Integration
Efficient background work with deduplication:
public class DailyNotificationWorker extends Worker {
private static final ConcurrentHashMap<String, AtomicBoolean> activeWork = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String, Long> workTimestamps = new ConcurrentHashMap<>();
@Override
public Result doWork() {
String workKey = createWorkKey(notificationId, action);
// Prevent duplicate work execution
if (!acquireWorkLock(workKey)) {
Log.d(TAG, "Skipping duplicate work: " + workKey);
return Result.success();
}
try {
// Check if work already completed
if (isWorkAlreadyCompleted(workKey)) {
Log.d(TAG, "Work already completed: " + workKey);
return Result.success();
}
// Execute actual work
Result result = executeWork();
// Mark as completed if successful
if (result == Result.success()) {
markWorkAsCompleted(workKey);
}
return result;
} finally {
releaseWorkLock(workKey);
}
}
}
Work Deduplication Strategy
Atomic locks prevent race conditions:
private boolean acquireWorkLock(String workKey) {
AtomicBoolean lock = activeWork.computeIfAbsent(workKey, k -> new AtomicBoolean(false));
boolean acquired = lock.compareAndSet(false, true);
if (acquired) {
workTimestamps.put(workKey, System.currentTimeMillis());
Log.d(TAG, "Acquired work lock: " + workKey);
}
return acquired;
}
private void releaseWorkLock(String workKey) {
AtomicBoolean lock = activeWork.remove(workKey);
workTimestamps.remove(workKey);
if (lock != null) {
lock.set(false);
Log.d(TAG, "Released work lock: " + workKey);
}
}
Database Performance
Query Optimization
Efficient queries with proper indexing:
// Optimized query for ready notifications
@Query("SELECT * FROM notification_content WHERE scheduled_time <= :currentTime AND delivery_status != 'delivered' ORDER BY scheduled_time ASC LIMIT 50")
List<NotificationContentEntity> getNotificationsReadyForDelivery(long currentTime);
// Optimized query for user notifications
@Query("SELECT * FROM notification_content WHERE timesafari_did = :timesafariDid AND created_at > :sinceTime ORDER BY scheduled_time ASC")
List<NotificationContentEntity> getRecentNotificationsByUser(String timesafariDid, long sinceTime);
// Optimized analytics query
@Query("SELECT notification_type, COUNT(*) as count FROM notification_content GROUP BY notification_type")
List<NotificationTypeCount> getNotificationCountsByType();
Background Thread Execution
Non-blocking operations for UI responsiveness:
public class DailyNotificationStorageRoom {
private final ExecutorService executorService = Executors.newFixedThreadPool(4);
public CompletableFuture<Boolean> saveNotificationContent(NotificationContentEntity content) {
return CompletableFuture.supplyAsync(() -> {
try {
contentDao.insertNotification(content);
return true;
} catch (Exception e) {
Log.e(TAG, "Failed to save notification", e);
return false;
}
}, executorService);
}
public CompletableFuture<List<NotificationContentEntity>> getNotificationsReadyForDelivery() {
return CompletableFuture.supplyAsync(() -> {
long currentTime = System.currentTimeMillis();
return contentDao.getNotificationsReadyForDelivery(currentTime);
}, executorService);
}
}
Memory Management
Efficient Data Structures
Memory-conscious data handling:
public class NotificationContentEntity {
// Use primitive types where possible
private long scheduledTime; // Instead of Date object
private int priority; // Instead of enum
private boolean vibrationEnabled; // Instead of Boolean
// Lazy loading for large fields
private String encryptedContent; // Load only when needed
private String metadata; // Parse only when accessed
// Efficient string operations
public boolean isExpired() {
long expirationTime = createdAt + (ttlSeconds * 1000);
return System.currentTimeMillis() > expirationTime;
}
}
Migration Strategy
From SharedPreferences to Room
Migration Process
Step-by-step migration from legacy storage:
public class StorageMigrationManager {
public void migrateToRoom(Context context) {
Log.d(TAG, "Starting migration from SharedPreferences to Room");
// 1. Export existing data
Map<String, Object> legacyData = exportSharedPreferencesData(context);
// 2. Transform to Room entities
List<NotificationContentEntity> entities = transformToRoomEntities(legacyData);
// 3. Import to Room database
importToRoomDatabase(entities);
// 4. Verify migration
boolean success = verifyMigration(entities.size());
// 5. Clean up legacy data
if (success) {
cleanupLegacyData(context);
Log.d(TAG, "Migration completed successfully");
} else {
Log.e(TAG, "Migration failed, keeping legacy data");
}
}
private Map<String, Object> exportSharedPreferencesData(Context context) {
SharedPreferences prefs = context.getSharedPreferences("DailyNotificationPrefs", Context.MODE_PRIVATE);
Map<String, ?> allData = prefs.getAll();
Map<String, Object> exportedData = new HashMap<>();
for (Map.Entry<String, ?> entry : allData.entrySet()) {
if (entry.getKey().startsWith("notification_")) {
exportedData.put(entry.getKey(), entry.getValue());
}
}
return exportedData;
}
}
Data Transformation
Convert legacy format to Room entities:
private List<NotificationContentEntity> transformToRoomEntities(Map<String, Object> legacyData) {
List<NotificationContentEntity> entities = new ArrayList<>();
for (Map.Entry<String, Object> entry : legacyData.entrySet()) {
try {
// Parse legacy JSON format
String jsonString = (String) entry.getValue();
JsonObject jsonObject = JsonParser.parseString(jsonString).getAsJsonObject();
// Create Room entity
NotificationContentEntity entity = new NotificationContentEntity();
entity.id = jsonObject.get("id").getAsString();
entity.title = jsonObject.get("title").getAsString();
entity.body = jsonObject.get("body").getAsString();
entity.scheduledTime = jsonObject.get("scheduledTime").getAsLong();
entity.timezone = jsonObject.get("timezone").getAsString();
entity.pluginVersion = PLUGIN_VERSION; // Set current version
entity.createdAt = System.currentTimeMillis();
entity.updatedAt = System.currentTimeMillis();
entities.add(entity);
} catch (Exception e) {
Log.w(TAG, "Failed to transform legacy data: " + entry.getKey(), e);
}
}
return entities;
}
Schema Evolution
Version Management
Database versioning for schema changes:
@Database(
entities = {
NotificationContentEntity.class,
NotificationDeliveryEntity.class,
NotificationConfigEntity.class
},
version = 1, // Increment for schema changes
exportSchema = false
)
public abstract class DailyNotificationDatabase extends RoomDatabase {
// Migration from version 1 to 2
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
// Add new columns
database.execSQL("ALTER TABLE notification_content ADD COLUMN analytics_data TEXT");
database.execSQL("ALTER TABLE notification_content ADD COLUMN priority_level INTEGER DEFAULT 0");
// Create new indexes
database.execSQL("CREATE INDEX idx_notification_content_priority ON notification_content(priority_level)");
}
};
}
Backward Compatibility
Graceful handling of schema changes:
public class SchemaCompatibilityManager {
public void ensureSchemaCompatibility() {
int currentVersion = getCurrentSchemaVersion();
int targetVersion = getTargetSchemaVersion();
if (currentVersion < targetVersion) {
Log.d(TAG, "Upgrading schema from version " + currentVersion + " to " + targetVersion);
performSchemaUpgrade(currentVersion, targetVersion);
}
}
private void performSchemaUpgrade(int fromVersion, int toVersion) {
for (int version = fromVersion + 1; version <= toVersion; version++) {
Migration migration = getMigration(version - 1, version);
if (migration != null) {
migration.migrate(database.getOpenHelper().getWritableDatabase());
Log.d(TAG, "Applied migration " + (version - 1) + " -> " + version);
}
}
}
}
Testing Architecture
Unit Testing Strategy
Entity Testing
Comprehensive entity validation:
@RunWith(AndroidJUnit4.class)
public class NotificationContentEntityTest {
@Test
public void testEntityCreation() {
NotificationContentEntity entity = new NotificationContentEntity(
"test-id", "1.0.0", "test-did", "reminder",
"Test Title", "Test Body", System.currentTimeMillis(), "UTC"
);
assertNotNull(entity.id);
assertEquals("test-id", entity.id);
assertEquals("1.0.0", entity.pluginVersion);
assertTrue(entity.isActive());
}
@Test
public void testExpirationLogic() {
NotificationContentEntity entity = new NotificationContentEntity();
entity.createdAt = System.currentTimeMillis() - (8 * 24 * 60 * 60 * 1000); // 8 days ago
entity.ttlSeconds = 7 * 24 * 60 * 60; // 7 days TTL
assertTrue(entity.isExpired());
}
@Test
public void testDeliveryReadiness() {
NotificationContentEntity entity = new NotificationContentEntity();
entity.scheduledTime = System.currentTimeMillis() - 1000; // 1 second ago
entity.deliveryStatus = "pending";
assertTrue(entity.isReadyForDelivery());
}
}
DAO Testing
Database operation testing:
@RunWith(AndroidJUnit4.class)
public class NotificationContentDaoTest {
private DailyNotificationDatabase database;
private NotificationContentDao dao;
@Before
public void createDatabase() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
database = Room.inMemoryDatabaseBuilder(context, DailyNotificationDatabase.class).build();
dao = database.notificationContentDao();
}
@After
public void closeDatabase() {
database.close();
}
@Test
public void testInsertAndRetrieve() {
NotificationContentEntity entity = createTestEntity();
dao.insertNotification(entity);
NotificationContentEntity retrieved = dao.getNotificationById(entity.id);
assertNotNull(retrieved);
assertEquals(entity.id, retrieved.id);
assertEquals(entity.title, retrieved.title);
}
@Test
public void testQueryByTimeSafariDid() {
NotificationContentEntity entity1 = createTestEntity("did1");
NotificationContentEntity entity2 = createTestEntity("did2");
dao.insertNotification(entity1);
dao.insertNotification(entity2);
List<NotificationContentEntity> did1Notifications = dao.getNotificationsByTimeSafariDid("did1");
assertEquals(1, did1Notifications.size());
assertEquals("did1", did1Notifications.get(0).timesafariDid);
}
}
Integration Testing
Plugin Integration Tests
End-to-end plugin testing:
@RunWith(AndroidJUnit4.class)
public class DailyNotificationPluginIntegrationTest {
private DailyNotificationPlugin plugin;
private Context context;
@Before
public void setup() {
context = InstrumentationRegistry.getInstrumentation().getTargetContext();
plugin = new DailyNotificationPlugin();
plugin.load();
}
@Test
public void testScheduleNotification() throws Exception {
JSObject options = new JSObject();
options.put("id", "test-notification");
options.put("title", "Test Notification");
options.put("body", "Test Body");
options.put("scheduledTime", System.currentTimeMillis() + 60000); // 1 minute from now
PluginCall call = new PluginCall(options, null, null);
plugin.scheduleDailyNotification(call);
// Verify notification was scheduled
assertTrue(call.getData().has("success"));
}
@Test
public void testCheckStatus() throws Exception {
PluginCall call = new PluginCall(new JSObject(), null, null);
plugin.checkStatus(call);
// Verify status response
JSObject result = call.getData();
assertTrue(result.has("enabled"));
assertTrue(result.has("permissions"));
}
}
Performance Testing
Database Performance Tests
Query performance validation:
@RunWith(AndroidJUnit4.class)
public class DatabasePerformanceTest {
@Test
public void testBulkInsertPerformance() {
List<NotificationContentEntity> entities = createBulkTestData(1000);
long startTime = System.currentTimeMillis();
dao.insertNotifications(entities);
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
assertTrue("Bulk insert took too long: " + duration + "ms", duration < 5000);
}
@Test
public void testQueryPerformance() {
// Insert test data
insertTestData(1000);
long startTime = System.currentTimeMillis();
List<NotificationContentEntity> results = dao.getNotificationsReadyForDelivery(System.currentTimeMillis());
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
assertTrue("Query took too long: " + duration + "ms", duration < 1000);
}
}
Deployment Architecture
Build Configuration
Gradle Configuration
Optimized build setup for production:
android {
compileSdkVersion 34
buildToolsVersion "34.0.0"
defaultConfig {
minSdkVersion 21
targetSdkVersion 34
// Plugin-specific configuration
buildConfigField "String", "PLUGIN_VERSION", "\"1.0.0\""
buildConfigField "boolean", "ENABLE_ANALYTICS", "true"
buildConfigField "boolean", "ENABLE_ENCRYPTION", "true"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
// Production-specific settings
buildConfigField "boolean", "DEBUG_MODE", "false"
buildConfigField "int", "MAX_NOTIFICATIONS", "10000"
}
debug {
minifyEnabled false
buildConfigField "boolean", "DEBUG_MODE", "true"
buildConfigField "int", "MAX_NOTIFICATIONS", "100"
}
}
}
dependencies {
// Room database
implementation "androidx.room:room-runtime:2.6.1"
annotationProcessor "androidx.room:room-compiler:2.6.1"
// WorkManager
implementation "androidx.work:work-runtime:2.9.0"
// Encryption
implementation "androidx.security:security-crypto:1.1.0-alpha06"
// Testing
testImplementation "junit:junit:4.13.2"
androidTestImplementation "androidx.test.ext:junit:1.1.5"
androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
}
ProGuard Configuration
Code obfuscation for production builds:
# Room database
-keep class * extends androidx.room.RoomDatabase
-keep @androidx.room.Entity class *
-keep @androidx.room.Dao class *
# Plugin classes
-keep class com.timesafari.dailynotification.** { *; }
# Capacitor plugin
-keep class com.timesafari.dailynotification.DailyNotificationPlugin { *; }
# Encryption
-keep class javax.crypto.** { *; }
-keep class java.security.** { *; }
# WorkManager
-keep class androidx.work.** { *; }
Plugin Distribution
Capacitor Plugin Structure
Standard plugin structure for distribution:
daily-notification-plugin/
├── android/
│ └── plugin/
│ ├── build.gradle
│ └── src/main/java/com/timesafari/dailynotification/
│ ├── DailyNotificationPlugin.java
│ ├── entities/
│ ├── dao/
│ ├── database/
│ └── storage/
├── src/
│ ├── definitions/
│ ├── web/
│ └── services/
├── package.json
├── capacitor.config.ts
└── README.md
Package Configuration
Plugin package metadata:
{
"name": "@timesafari/daily-notification",
"version": "1.0.0",
"description": "Enterprise-grade daily notification plugin for TimeSafari",
"main": "dist/plugin.cjs.js",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"capacitor": {
"platforms": {
"android": {
"src": "android"
},
"ios": {
"src": "ios"
}
}
},
"keywords": [
"capacitor",
"plugin",
"notifications",
"timesafari",
"android",
"ios"
],
"author": "Matthew Raymer",
"license": "MIT"
}
Monitoring & Analytics
Analytics Architecture
Delivery Analytics
Comprehensive delivery tracking:
public class DeliveryAnalyticsManager {
public DeliveryAnalytics calculateAnalytics(String timesafariDid) {
List<NotificationDeliveryEntity> deliveries = deliveryDao.getDeliveriesByTimeSafariDid(timesafariDid);
int totalDeliveries = deliveries.size();
int successfulDeliveries = 0;
int failedDeliveries = 0;
long totalDuration = 0;
int userInteractions = 0;
for (NotificationDeliveryEntity delivery : deliveries) {
if (delivery.isSuccessful()) {
successfulDeliveries++;
totalDuration += delivery.deliveryDurationMs;
} else {
failedDeliveries++;
}
if (delivery.hasUserInteraction()) {
userInteractions++;
}
}
double successRate = totalDeliveries > 0 ? (double) successfulDeliveries / totalDeliveries : 0.0;
double averageDuration = successfulDeliveries > 0 ? (double) totalDuration / successfulDeliveries : 0.0;
double interactionRate = totalDeliveries > 0 ? (double) userInteractions / totalDeliveries : 0.0;
return new DeliveryAnalytics(
totalDeliveries, successfulDeliveries, failedDeliveries,
successRate, averageDuration, userInteractions, interactionRate
);
}
}
Performance Metrics
System performance monitoring:
public class PerformanceMetricsManager {
public PerformanceMetrics collectMetrics() {
return new PerformanceMetrics(
getDatabaseSize(),
getQueryPerformanceMetrics(),
getMemoryUsageMetrics(),
getBatteryImpactMetrics()
);
}
private DatabaseSizeMetrics getDatabaseSize() {
return new DatabaseSizeMetrics(
contentDao.getTotalNotificationCount(),
deliveryDao.getTotalDeliveryCount(),
configDao.getTotalConfigCount()
);
}
private QueryPerformanceMetrics getQueryPerformanceMetrics() {
long startTime = System.currentTimeMillis();
List<NotificationContentEntity> results = contentDao.getNotificationsReadyForDelivery(System.currentTimeMillis());
long endTime = System.currentTimeMillis();
return new QueryPerformanceMetrics(
results.size(),
endTime - startTime
);
}
}
Logging Architecture
Structured Logging
Comprehensive logging for debugging and monitoring:
public class NotificationLogger {
private static final String TAG = "DailyNotification";
public static void logNotificationScheduled(String notificationId, long scheduledTime) {
Log.d(TAG, String.format("DN|SCHEDULE notification_id=%s scheduled_time=%d",
notificationId, scheduledTime));
}
public static void logNotificationDelivered(String notificationId, long durationMs) {
Log.d(TAG, String.format("DN|DELIVERED notification_id=%s duration_ms=%d",
notificationId, durationMs));
}
public static void logNotificationFailed(String notificationId, String errorCode, String errorMessage) {
Log.e(TAG, String.format("DN|FAILED notification_id=%s error_code=%s error_message=%s",
notificationId, errorCode, errorMessage));
}
public static void logUserInteraction(String notificationId, String interactionType) {
Log.d(TAG, String.format("DN|INTERACTION notification_id=%s interaction_type=%s",
notificationId, interactionType));
}
}
Error Tracking
Comprehensive error tracking for debugging:
public class ErrorTrackingManager {
public void trackError(String errorCode, String errorMessage, Exception exception) {
ErrorReport report = new ErrorReport(
errorCode,
errorMessage,
exception != null ? exception.getMessage() : null,
System.currentTimeMillis(),
getDeviceContext(),
getPluginContext()
);
// Store error report
storeErrorReport(report);
// Log error
Log.e(TAG, "Error tracked: " + errorCode + " - " + errorMessage, exception);
}
private DeviceContext getDeviceContext() {
return new DeviceContext(
Build.VERSION.SDK_INT,
Build.MANUFACTURER,
Build.MODEL,
getBatteryLevel(),
isDozeModeActive()
);
}
}
Future Architecture
Planned Enhancements
iOS Implementation
Cross-platform parity with iOS:
// Planned iOS implementation
@objc(DailyNotificationPlugin)
public class DailyNotificationPlugin: CAPPlugin {
private var scheduler: DailyNotificationScheduler?
private var storage: DailyNotificationStorageRoom?
@objc func scheduleDailyNotification(_ call: CAPPluginCall) {
// iOS-specific implementation
// - UNUserNotificationCenter integration
// - BGTaskScheduler for background work
// - Core Data for persistence
}
}
Advanced Features
Enterprise-grade enhancements:
-
Push Notification Integration
- FCM/APNs integration
- Server-side notification management
- Real-time delivery status
-
Advanced Analytics
- Machine learning insights
- User behavior analysis
- Performance optimization recommendations
-
Multi-User Support
- Shared notification management
- Team collaboration features
- Admin dashboard integration
-
Cloud Synchronization
- Cross-device notification sync
- Backup and restore
- Conflict resolution
Scalability Considerations
Horizontal Scaling
Multi-instance support for high-volume applications:
public class MultiInstanceManager {
public void initializeInstance(String instanceId) {
// Create instance-specific database
String databaseName = "daily_notification_" + instanceId + ".db";
DailyNotificationDatabase instanceDb = Room.databaseBuilder(
context, DailyNotificationDatabase.class, databaseName
).build();
// Initialize instance-specific services
initializeInstanceServices(instanceId, instanceDb);
}
}
Performance Optimization
Advanced performance features:
-
Database Sharding
- User-based sharding
- Time-based partitioning
- Load balancing
-
Caching Strategy
- Redis integration
- In-memory caching
- Cache invalidation
-
Background Processing
- Distributed work queues
- Priority-based processing
- Resource management
Conclusion
The DailyNotification plugin architecture provides a robust, scalable, and maintainable foundation for enterprise-grade notification management. With its Room database integration, comprehensive analytics, security-first design, and performance optimization, it delivers reliable notification services for TimeSafari applications.
Key Architectural Strengths
- Data Integrity: Schema validation and encryption ensure data security
- Performance: Optimized queries and background processing for responsiveness
- Reliability: Work deduplication and retry logic for consistent operation
- Scalability: Modular design supports future enhancements
- Maintainability: Clean separation of concerns and comprehensive testing
Next Steps
- Complete Room Migration: Finish migration from SharedPreferences to Room database
- iOS Implementation: Port Android architecture to iOS platform
- Advanced Analytics: Implement machine learning insights
- Cloud Integration: Add server-side synchronization capabilities
- Performance Optimization: Implement advanced caching and sharding
This architecture provides a solid foundation for building reliable, scalable notification services that meet enterprise requirements while maintaining excellent user experience.
Document Version: 1.0.0
Last Updated: October 20, 2025
Next Review: November 20, 2025