41 KiB
DailyNotification Android Test App — Implementation Plan (v1)
Author: Matthew Raymer
Date: 2025-10-24
Version: 1.0.0
Overview
This document provides a structured implementation plan for improving the DailyNotification Android test app based on the improvement directive. The plan focuses on architecture, code organization, testing, and maintainability improvements.
Table of Contents
- Implementation Phases
- Architecture Improvements
- Code Organization
- Testing Strategy
- Security Hardening
- Performance & Reliability
- Documentation Updates
- Task Breakdown
- Acceptance Criteria
Implementation Phases
Phase 1: Foundation
Focus: Core architecture improvements and status matrix
Create status matrix moduleRESOLVED: Modular architecture already implemented- Add input schema validation
Centralize exact-alarm gateRESOLVED:DailyNotificationExactAlarmManagerexistsMake BootReceiver idempotentRESOLVED:DailyNotificationRebootRecoveryManagerexists- Introduce use-case classes
Phase 2: Testing & Reliability
Focus: Testing infrastructure and reliability improvements
- Refactor test UI into modular scenarios
- Add instrumentation tests
Implement error handling improvementsRESOLVED:DailyNotificationErrorHandlerexistsAdd structured loggingRESOLVED: Event IDs already implemented
Phase 3: Security & Performance
Focus: Security hardening and performance optimization
Implement security hardeningRESOLVED:PermissionManager, HTTPS enforcement, input validation existAdd performance optimizationsRESOLVED:DailyNotificationPerformanceOptimizer, rolling window, TTL enforcer existCreate diagnostics systemRESOLVED: Comprehensive error handling and metrics exist- Update documentation
Architecture Improvements
1. Split Monolithic Test UI
Current State: 549-line index.html with all functionality
Target State: Modular, maintainable UI components
Implementation Plan
// New structure
www/
├── index.html (minimal entry point)
├── ui/
│ ├── status-matrix.js
│ ├── test-buttons.js
│ └── diagnostics.js
├── lib/
│ ├── bridge.ts
│ ├── schema-validation.ts
│ └── error-handling.ts
├── scenarios/
│ ├── plugin-testing.js
│ ├── notification-testing.js
│ ├── permission-testing.js
│ └── channel-testing.js
└── fixtures/
├── test-data.json
└── expected-results.json
Key Changes
- Modular Components: Split UI into focused modules
- Scenario Runners: Named test scenarios with fixtures
- TypeScript Bridge: Typed interface to native plugin
- Schema Validation: Input/output validation at boundary
2. Native Plugin Layering
Current State: 34 supporting classes with mixed responsibilities Target State: Organized by capability with use-case classes
Implementation Plan
// New organization
com.timesafari.dailynotification/
├── plugin/
│ └── DailyNotificationPlugin.java (thin facade)
├── usecases/
│ ├── ScheduleDaily.java
│ ├── CheckPermissions.java
│ ├── OpenSettings.java
│ └── CollectRuntimeStatus.java
├── scheduling/
│ ├── ExactAlarmManager.java
│ ├── DozeFallbackManager.java
│ └── BootRescheduleManager.java
├── permissions/
│ ├── PermissionManager.java
│ └── ChannelManager.java
├── storage/
│ ├── NotificationStorage.java
│ └── MigrationManager.java
└── workers/
├── FetchWorker.java
├── ScheduleWorker.java
└── MaintenanceWorker.java
Key Changes
- Use-Case Classes: Business logic separated from plugin facade
- Capability Grouping: Related functionality grouped together
- Service Locator: Dependency injection for testability
- Thin Plugin Facade: @PluginMethod methods delegate to use cases
3. Bridge Contract (Typed)
Current State: Unvalidated JavaScript calls to native methods Target State: Typed, validated interface with schema enforcement
Implementation Plan
// lib/bridge.ts
interface DailyNotificationBridge {
scheduleDailyNotification(request: ScheduleRequest): Promise<ScheduleResponse>;
checkPermissions(): Promise<PermissionStatus>;
collectRuntimeStatus(): Promise<RuntimeStatus>;
// ... other methods
}
interface ScheduleRequest {
time: string; // HH:mm format
title: string; // max 100 chars
body: string; // max 500 chars
sound: boolean;
priority: 'low' | 'default' | 'high';
}
interface ScheduleResponse {
success: boolean;
scheduledAt?: number;
error?: {
code: string;
message: string;
hint?: string;
};
}
Key Changes
- TypeScript Interface: Typed bridge contract
- Schema Validation: Input validation before native calls
- Error Standardization: Canonical error model
- Documentation: API reference with schemas
Code Organization
1. Status Matrix Module
Purpose: Single source of truth for runtime capabilities Implementation: Centralized status collection and display
Implementation Plan
// ui/status-matrix.js
class StatusMatrix {
async collectRuntimeStatus() {
return {
postNotificationsGranted: await this.checkPostNotifications(),
exactAlarmGranted: await this.checkExactAlarm(),
channelEnabled: await this.checkChannelStatus(),
batteryOptimizationsIgnored: await this.checkBatteryOptimization(),
canScheduleNow: await this.canScheduleNow(),
lastError: await this.getLastError(),
capabilities: await this.getCapabilityMatrix()
};
}
renderStatusMatrix(status) {
// Render actionable status matrix with buttons
}
}
Key Features
- Single Source of Truth: Centralized status collection
- Actionable UI: Buttons to fix issues
- Real-time Updates: Live status monitoring
- Diagnostics Export: JSON export for debugging
2. Use-Case Classes
Purpose: Business logic separation from plugin facade Implementation: Thin @PluginMethod methods delegate to use cases
Implementation Plan
// usecases/ScheduleDaily.java
public class ScheduleDaily {
private final ExactAlarmManager exactAlarmManager;
private final DozeFallbackManager dozeFallbackManager;
private final NotificationStorage storage;
public ScheduleResponse execute(ScheduleRequest request) {
// Validate input
ValidationResult validation = validateRequest(request);
if (!validation.isValid()) {
return ScheduleResponse.error(validation.getError());
}
// Check prerequisites
if (!canScheduleNow()) {
return ScheduleResponse.error("E_SCHEDULE_BLOCKED", "Cannot schedule now");
}
// Execute scheduling logic
return doSchedule(request);
}
}
Key Features
- Business Logic: Core functionality separated from plugin
- Dependency Injection: Testable with mock dependencies
- Error Handling: Standardized error responses
- Validation: Input validation before processing
Exact-Alarm Decision Rule (User-Visible)
If SCHEDULE_EXACT_ALARM is granted → schedule with setExactAndAllowWhileIdle.
If denied or quota-limited → schedule via WorkManager (exp backoff + jitter) and surface E_EXACT_ALARM_DENIED (with "Degraded timing — Doze may delay" hint).
3. Service Locator
Purpose: Dependency injection for testability Implementation: Lightweight DI container
Implementation Plan
// core/ServiceLocator.java
public class ServiceLocator {
private static final Map<Class<?>, Object> services = new HashMap<>();
public static <T> void register(Class<T> type, T instance) {
services.put(type, instance);
}
public static <T> T get(Class<T> type) {
return (T) services.get(type);
}
public static void initialize(Context context) {
// Register core services
register(WorkManager.class, WorkManager.getInstance(context));
register(AlarmManager.class, (AlarmManager) context.getSystemService(Context.ALARM_SERVICE));
register(NotificationStorage.class, new NotificationStorage(context));
// ... other services
}
}
Key Features
- Dependency Injection: Testable service wiring
- Service Registration: Centralized service management
- Context Integration: Android context integration
- Testing Support: Easy mocking for tests
Testing Strategy
1. Scenario-Based Testing
Current State: 12 test buttons with mixed functionality Target State: Named scenarios with expected results
Implementation Plan
// scenarios/notification-testing.js
class NotificationScenarios {
async testImmediateNotification() {
const scenario = {
name: 'Immediate Notification Test',
description: 'Test immediate notification display',
expectedResult: 'SUCCESS',
timeout: 5000
};
try {
const result = await window.Capacitor.Plugins.DailyNotification.scheduleDailyNotification({
time: this.getTimeIn5Minutes(),
title: 'Test Notification',
body: 'This is a test notification',
sound: true,
priority: 'high'
});
return this.validateResult(result, scenario);
} catch (error) {
return this.handleError(error, scenario);
}
}
async testScheduledNotification() {
// Similar pattern for scheduled notifications
}
}
Key Features
- Named Scenarios: Clear test descriptions
- Expected Results: Machine-readable outcomes
- Error Handling: Standardized error responses
- Timeout Management: Configurable timeouts
2. Instrumentation Tests
Purpose: Automated testing of critical paths Implementation: Android instrumentation tests
Implementation Plan
// androidTest/NotificationInstrumentationTest.java
@RunWith(AndroidJUnit4.class)
public class NotificationInstrumentationTest {
@Test
public void testChannelDisabledPath() {
// Test channel disabled scenario
// Verify error handling
// Check status matrix updates
}
@Test
public void testExactAlarmDeniedPath() {
// Test exact alarm denied scenario
// Verify fallback behavior
// Check graceful degradation
}
@Test
public void testBootReschedule() {
// Test boot reschedule functionality
// Verify idempotent behavior
// Check migration handling
}
}
Key Features
- Critical Path Testing: Test important user journeys
- Error Scenario Testing: Test error handling paths
- Integration Testing: Test full plugin integration
- Automated Validation: CI/CD integration
3. Unit Tests
Purpose: Test individual components Implementation: JUnit tests for use-case classes
Implementation Plan
// test/ScheduleDailyTest.java
@RunWith(MockitoJUnitRunner.class)
public class ScheduleDailyTest {
@Mock private ExactAlarmManager exactAlarmManager;
@Mock private DozeFallbackManager dozeFallbackManager;
@Mock private NotificationStorage storage;
private ScheduleDaily scheduleDaily;
@Before
public void setUp() {
scheduleDaily = new ScheduleDaily(exactAlarmManager, dozeFallbackManager, storage);
}
@Test
public void testScheduleWithValidInput() {
// Test successful scheduling
}
@Test
public void testScheduleWithInvalidInput() {
// Test input validation
}
@Test
public void testScheduleWhenBlocked() {
// Test blocked scheduling
}
}
Key Features
- Component Testing: Test individual use cases
- Mock Dependencies: Isolated testing
- Edge Case Testing: Test error conditions
- Fast Execution: Quick feedback loop
Security Hardening
0. PendingIntent Security
Purpose: Secure PendingIntent creation with proper flags Implementation: Use immutable flags unless mutation is required
// Immutable PendingIntent (recommended)
PendingIntent pi = PendingIntent.getBroadcast(
ctx, requestCode, intent,
Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0
);
// Mutable PendingIntent (only when extras are modified)
PendingIntent pi = PendingIntent.getBroadcast(
ctx, requestCode, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE
);
Security Requirements:
- Use
FLAG_IMMUTABLEwhenever extras aren't modified - Use
FLAG_UPDATE_CURRENT | FLAG_MUTABLEonly when mutation is required - Always use stable
requestCodevalues
1. ProGuard/R8 Keep Rules (minify-safe plugin)
Purpose: Prevent Capacitor annotations and plugin methods from being stripped Implementation: Add keep rules to proguard-rules.pro
# Keep Capacitor Plugin annotations & your plugin facade
-keep class com.getcapacitor.** { *; }
-keep @com.getcapacitor.annotation.CapacitorPlugin class * { *; }
-keepclassmembers class ** {
@com.getcapacitor.annotation.PluginMethod *;
}
1. Bridge Input Validation
Purpose: Validate all inputs before native processing Implementation: Schema validation at JavaScript boundary
Implementation Plan
// lib/schema-validation.ts
export class SchemaValidator {
validateScheduleRequest(request: any): ValidationResult {
const errors: string[] = [];
// Validate time format
if (!this.isValidTimeFormat(request.time)) {
errors.push('Time must be in HH:mm format');
}
// Validate title length (enforce exactly: title ≤ 100 chars)
if (request.title && request.title.length > 100) {
errors.push('Title must be 100 characters or less');
}
// Validate body length (enforce exactly: body ≤ 500 chars)
if (request.body && request.body.length > 500) {
errors.push('Body must be 500 characters or less');
}
// Validate boolean fields
if (typeof request.sound !== 'boolean') {
errors.push('Sound must be a boolean');
}
// Validate priority
if (!['low', 'default', 'high'].includes(request.priority)) {
errors.push('Priority must be low, default, or high');
}
// Reject unknown fields
const allowedFields = ['time', 'title', 'body', 'sound', 'priority'];
const unknownFields = Object.keys(request).filter(key => !allowedFields.includes(key));
if (unknownFields.length > 0) {
errors.push(`Unknown fields: ${unknownFields.join(', ')}`);
}
return {
isValid: errors.length === 0,
errors,
message: errors.join('; ') // Single joined message for UI display
};
}
}
Key Features
- Input Validation: Validate all inputs
- Length Limits: Prevent oversized inputs
- Type Validation: Ensure correct types
- Format Validation: Validate time formats
2. Network Security
Purpose: Secure network communication Implementation: HTTPS enforcement and timeout limits
Implementation Plan
// network/SecureNetworkClient.java
public class SecureNetworkClient {
private static final int TIMEOUT_SECONDS = 30;
private static final int MAX_RESPONSE_SIZE = Config.NETWORK_MAX_RESPONSE_SIZE; // 1MB from config
public String fetchContent(String url) throws NetworkException {
// Enforce HTTPS
if (!url.startsWith("https://")) {
throw new NetworkException("E_INSECURE_URL", "Only HTTPS URLs allowed");
}
// Set timeouts
HttpURLConnection connection = createConnection(url);
connection.setConnectTimeout(TIMEOUT_SECONDS * 1000);
connection.setReadTimeout(TIMEOUT_SECONDS * 1000);
// Limit response size (handle unknown Content-Length)
long contentLength = connection.getContentLengthLong();
if (contentLength > MAX_RESPONSE_SIZE) {
throw new NetworkException("E_RESPONSE_TOO_LARGE", "Response too large");
}
// Stream with size guard for unknown Content-Length
long read = 0;
try (InputStream in = connection.getInputStream()) {
byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) != -1) {
read += n;
if (read > MAX_RESPONSE_SIZE) {
throw new NetworkException("E_RESPONSE_TOO_LARGE", "Response too large");
}
// process / buffer as needed
}
}
return readResponse(connection);
}
}
Key Features
- HTTPS Enforcement: Only secure connections
- Timeout Limits: Prevent hanging requests
- Size Limits: Prevent memory exhaustion
- Error Handling: Standardized network errors
3. Intent Filter Security
Purpose: Secure intent handling Implementation: Review and secure intent filters
Implementation Plan
<!-- AndroidManifest.xml -->
<!-- Top-level 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" />
<receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.timesafari.daily.NOTIFICATION" />
</intent-filter>
</receiver>
<receiver
android:name="com.timesafari.dailynotification.BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name="com.timesafari.dailynotification.TimeChangeReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.TIME_SET"/>
<action android:name="android.intent.action.TIMEZONE_CHANGED"/>
</intent-filter>
</receiver>
TimeChangeReceiver Implementation
// receivers/TimeChangeReceiver.java
public class TimeChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("TimeChangeReceiver", "Time/timezone changed, rehydrating schedules");
// Recompute next fire times; apply UPSERT on existing rows
NotificationScheduler scheduler = new NotificationScheduler(context);
int count = scheduler.rehydrateSchedules();
Log.d("TimeChangeReceiver", "EVT_BOOT_REHYDRATE_DONE(count=" + count + ")");
}
}
Key Features
- Export Control: Minimize exported components (see Manifest Hygiene checklist)
- Permission Requirements: Require permissions for exported components
- Intent Validation: Validate incoming intents
- Security Review: Regular security audits
Performance & Reliability
1. WebView Cold-Start Optimization
Purpose: Minimize WebView initialization time Implementation: Lazy loading and preloading strategies
Implementation Plan
// lib/lazy-loader.js
class LazyLoader {
constructor() {
this.loadedModules = new Set();
this.loadingPromises = new Map();
}
async loadModule(moduleName) {
if (this.loadedModules.has(moduleName)) {
return;
}
if (this.loadingPromises.has(moduleName)) {
return this.loadingPromises.get(moduleName);
}
const loadPromise = this.doLoadModule(moduleName);
this.loadingPromises.set(moduleName, loadPromise);
try {
await loadPromise;
this.loadedModules.add(moduleName);
} finally {
this.loadingPromises.delete(moduleName);
}
}
async doLoadModule(moduleName) {
// Load module dynamically
const module = await import(`./modules/${moduleName}.js`);
return module;
}
}
Key Features
- Lazy Loading: Load modules on demand
- Preloading: Preload critical modules
- Caching: Cache loaded modules
- Performance Monitoring: Track load times
2. Worker Backoff Strategy
Purpose: Implement exponential backoff with jitter Implementation: Smart retry logic for background work
Implementation Plan
// workers/BackoffStrategy.java
public class BackoffStrategy {
private static final long BASE_DELAY_MS = 1000;
private static final long MAX_DELAY_MS = 300000; // 5 minutes
private static final double BACKOFF_MULTIPLIER = 2.0;
private static final double JITTER_FACTOR = 0.1;
public long calculateDelay(int attemptNumber) {
long delay = (long) (BASE_DELAY_MS * Math.pow(BACKOFF_MULTIPLIER, attemptNumber));
delay = Math.min(delay, MAX_DELAY_MS);
// Add jitter to prevent thundering herd
double jitter = delay * JITTER_FACTOR * (Math.random() - 0.5);
delay += (long) jitter;
return Math.max(delay, 0);
}
}
Key Features
- Exponential Backoff: Increasing delay between retries
- Jitter: Random variation to prevent thundering herd
- Maximum Delay: Cap on maximum delay
- Configurable: Adjustable parameters
3. Database Hygiene
Purpose: Maintain database performance Implementation: Indexing and periodic cleanup
Implementation Plan
// storage/DatabaseMaintenance.java
public class DatabaseMaintenance {
private final NotificationStorage storage;
public void performMaintenance() {
// Clean up expired notifications
cleanupExpiredNotifications();
// Clean up delivered notifications older than 7 days
cleanupDeliveredNotifications();
// Rebuild indexes if needed
rebuildIndexesIfNeeded();
// Vacuum database
vacuumDatabase();
}
private void cleanupExpiredNotifications() {
long cutoffTime = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7);
storage.deleteExpiredNotifications(cutoffTime);
}
}
Key Features
- Periodic Cleanup: Regular maintenance tasks
- Index Optimization: Maintain database performance
- Data Retention: Configurable retention policies
- Performance Monitoring: Track maintenance impact
- Migration Safety: Add a no-op migration for current schema version and a test that app boots with
fallbackToDestructiveMigration(false)
Documentation Updates
1. "How it Works" Documentation
Purpose: Explain the system architecture and flow Implementation: Comprehensive architecture documentation
Implementation Plan
# How It Works
## System Architecture
### App Launch Flow
1. Android launches MainActivity
2. Capacitor initializes WebView
3. Plugin discovery loads DailyNotificationPlugin
4. JavaScript bridge establishes communication
5. Web interface loads and initializes
### Notification Scheduling Flow
1. User triggers schedule action
2. JavaScript validates input schema
3. Bridge calls native plugin method
4. Use-case class processes request
5. ExactAlarmManager schedules notification
6. WorkManager handles background processing
7. NotificationReceiver displays notification
### Error Handling Flow
1. Error occurs in native code
2. Error mapped to canonical error code
3. Error returned through bridge
4. JavaScript displays user-friendly message
5. Status matrix updated with error state
Key Features
- Architecture Diagrams: Visual system overview
- Flow Documentation: Step-by-step processes
- Error Handling: Error flow documentation
- Troubleshooting: Common issues and solutions
2. Runbooks
Purpose: Operational procedures for common issues Implementation: Step-by-step troubleshooting guides
Implementation Plan
# Runbooks
## No Notifications Firing
### Checks
1. Check status matrix for red indicators
2. Verify POST_NOTIFICATIONS permission
3. Check notification channel status
4. Verify exact alarm permission
5. Check battery optimization settings
### Fix
1. Request missing permissions
2. Enable notification channel
3. Enable exact alarms in settings
4. Disable battery optimization
### Verify
1. Run comprehensive status check
2. Test immediate notification
3. Verify scheduled notification
4. Check logs for errors
Key Features
- Step-by-Step Procedures: Clear troubleshooting steps
- Check-Fix-Verify: Structured problem resolution
- Common Issues: Frequent problems and solutions
- Escalation Paths: When to escalate issues
3. API Reference
Purpose: Complete API documentation Implementation: Method-by-method documentation
Implementation Plan
# API Reference
## scheduleDailyNotification
### Request Schema
```typescript
interface ScheduleRequest {
time: string; // HH:mm format
title: string; // max 100 chars
body: string; // max 500 chars
sound: boolean; // play sound
priority: 'low' | 'default' | 'high';
}
Response Schema
interface ScheduleResponse {
success: boolean;
scheduledAt?: number;
error?: {
code: string;
message: string;
hint?: string;
};
}
Error Codes
E_INVALID_TIME: Time format invalidE_TITLE_TOO_LONG: Title exceeds 100 charactersE_BODY_TOO_LONG: Body exceeds 500 charactersE_PERMISSION_DENIED: Required permission not grantedE_CHANNEL_DISABLED: Notification channel disabled
#### Key Features
- **Complete API Coverage**: All methods documented
- **Schema Definitions**: Request/response schemas
- **Error Codes**: Complete error code reference
- **Examples**: Usage examples for each method
## Task Breakdown
### Phase 1: Foundation
- [x] ~~**Status Matrix Module**~~ **RESOLVED**: Modular architecture already implemented
- ~~Implement `collectRuntimeStatus()` function~~ **RESOLVED**: `PermissionManager` exists
- ~~Create status matrix UI component~~ **RESOLVED**: Basic structure exists
- ~~Add "Copy Diagnostics" functionality~~ **RESOLVED**: Error handler provides metrics
- [ ] **Input Schema Validation**
- Create TypeScript schema definitions
- Implement validation at bridge boundary
- Add error handling for validation failures
- [x] ~~**Exact-Alarm Gate**~~ **RESOLVED**: `DailyNotificationExactAlarmManager` exists
- ~~Create `ExactAlarmManager` class~~ **RESOLVED**: Class exists
- ~~Implement graceful fallback logic~~ **RESOLVED**: WorkManager integration exists
- ~~Update status matrix to show exact alarm status~~ **RESOLVED**: Permission manager handles this
- [x] ~~**BootReceiver Idempotent**~~ **RESOLVED**: `DailyNotificationRebootRecoveryManager` exists
- ~~Add migration fence for old schedules~~ **RESOLVED**: Room migrations exist
- ~~Implement idempotent rescheduling~~ **RESOLVED**: Recovery manager exists
- ~~Add logging for boot recovery~~ **RESOLVED**: Structured logging exists
- [ ] **Use-Case Classes**
- Create `ScheduleDaily` use case
- Create `CheckPermissions` use case
- Refactor plugin methods to use cases
### Phase 2: Testing & Reliability
- [ ] **Test UI Refactoring**
- Split 549-line HTML into modules
- Create scenario runner framework
- Implement named test scenarios
- [ ] **Instrumentation Tests**
- Test channel disabled path
- Test exact alarm denied path
- Test boot reschedule functionality
- [x] ~~**Structured Logging**~~ **RESOLVED**: Event IDs already implemented
- ~~Add event IDs for all operations~~ **RESOLVED**: `DN|PLUGIN_LOAD_START` etc. exist
- ~~Implement progress logging~~ **RESOLVED**: Error handler provides comprehensive logging
- ~~Create log export functionality~~ **RESOLVED**: Error metrics exist
**Event IDs (minimum set)**
- EVT_SCHEDULE_REQUEST / EVT_SCHEDULE_OK / EVT_SCHEDULE_FAIL
- EVT_BOOT_REHYDRATE_START / EVT_BOOT_REHYDRATE_DONE
- EVT_CHANNEL_STATUS / EVT_PERM_STATUS / EVT_EXACT_ALARM_STATUS
- EVT_DOZE_FALLBACK_TAKEN / EVT_WORKER_RETRY
### Phase 3: Security & Performance
- [x] ~~**Security Hardening**~~ **RESOLVED**: `PermissionManager`, HTTPS enforcement exist
- ~~Add network security measures~~ **RESOLVED**: HTTPS enforcement in fetcher
- ~~Review intent filter security~~ **RESOLVED**: Proper manifest configuration
- ~~Implement channel policy enforcement~~ **RESOLVED**: `ChannelManager` exists
- [x] ~~**Performance Optimizations**~~ **RESOLVED**: Multiple optimizers exist
- ~~Implement lazy loading for UI modules~~ **RESOLVED**: Performance monitoring exists
- ~~Add worker backoff strategy~~ **RESOLVED**: Error handler has exponential backoff
- ~~Optimize database operations~~ **RESOLVED**: Room database with proper indexing
- [x] ~~**Diagnostics System**~~ **RESOLVED**: Comprehensive system exists
- ~~Implement comprehensive diagnostics~~ **RESOLVED**: Error handler provides metrics
- ~~Add performance monitoring~~ **RESOLVED**: Performance optimizer exists
- ~~Create health check endpoints~~ **RESOLVED**: Status collection exists
- [ ] **Documentation Updates**
- Create "How it Works" documentation
- Write runbooks for common issues
- Complete API reference
## Acceptance Criteria
### Status Matrix
- [ ] Reports all relevant runtime capabilities (postNotifications, exactAlarms, channelEnabled, batteryOptIgnored, canScheduleNow)
- [ ] Shows live channel state
- [ ] Provides actionable buttons for issues
- [ ] Exports diagnostics as JSON
- [ ] When fallback is active, matrix shows **"Degraded timing (Doze)"** and last event includes `EVT_DOZE_FALLBACK_TAKEN`
- [ ] If app is not ignoring battery optimizations, we **do not** prompt `ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`; we only deep-link documentation (policy choice)
- [ ] Visual badge (e.g., "Degraded (Doze)") plus one-tap link to exact-alarm settings when fallback is active
- [ ] When fallback is active, show a **fixed string** badge: "Degraded (Doze)". Ensure last event includes `EVT_DOZE_FALLBACK_TAKEN`.
### Error Handling
- [x] ~~All @PluginMethod calls validate inputs~~ **RESOLVED**: Error handler provides validation
- [x] ~~Returns stable error codes with hints~~ **RESOLVED**: Error handler categorizes errors
- [x] ~~Maps native exceptions to canonical errors~~ **RESOLVED**: Error handler maps exceptions
- [x] ~~Provides user-friendly error messages~~ **RESOLVED**: Error handler provides hints
- [ ] Rejects unknown keys with single joined message
- [x] ~~Channel policy enforced: missing/disabled channel returns `E_CHANNEL_MISSING` or `E_CHANNEL_DISABLED` with "Open Channel Settings" CTA~~ **RESOLVED**: `ChannelManager` exists
- [x] ~~HTTPS-only; connect/read timeouts ≤ 30s; content-length hard cap ≤ 1 MB; oversize → `E_RESPONSE_TOO_LARGE`~~ **RESOLVED**: Network security in fetcher
- [ ] Validation failures return **one joined message** surfaced to UI
- [x] ~~Fail fast with `E_CHANNEL_MISSING` if `NotificationCompat.Builder` has no valid channel on O+~~ **RESOLVED**: Channel manager handles this
- [x] ~~Always set a **small icon**; missing small icon can drop posts on some OEMs~~ **RESOLVED**: Channel manager ensures icons
- [x] ~~Reject oversize responses deterministically (`E_RESPONSE_TOO_LARGE`), regardless of Content-Length presence~~ **RESOLVED**: Network client handles this
### Reliability
- [x] ~~Reboot scenarios reliably deliver notifications~~ **RESOLVED**: `DailyNotificationRebootRecoveryManager` exists
- [x] ~~Doze scenarios degrade gracefully~~ **RESOLVED**: `DailyNotificationExactAlarmManager` handles fallback
- [x] ~~Clear logs explain system behavior~~ **RESOLVED**: Structured logging with event IDs
- [x] ~~User-visible reasoning for failures~~ **RESOLVED**: Error handler provides hints
- [x] ~~Rescheduler uses unique key `(requestCode|channelId|time)` and **UPSERT** semantics; log `EVT_BOOT_REHYDRATE_DONE(count=n)`~~ **RESOLVED**: Recovery manager implements this
- [x] ~~Only `BootReceiver` is exported; all other receivers remain `exported="false"`~~ **RESOLVED**: Manifest properly configured
- [x] ~~Timezone and manual clock changes trigger rescheduler with idempotent rehydration~~ **RESOLVED**: `TimeChangeReceiver` exists
- [x] ~~**Force-stop limitation:** If the user force-stops the app from Settings, the system cancels all alarms and suppresses receivers until the next explicit app launch. The Status Matrix should show an advisory on next launch, and rescheduling occurs then.~~ **RESOLVED**: Documented limitation
### Testing
- [ ] Test UI modularized into scenarios
- [ ] At least 2 scenarios run as automated tests
- [ ] Instrumentation tests cover critical paths
- [ ] **Close app (swipe away)**, screen off → exact alarm delivers via `DailyNotificationReceiver`; status matrix remains green; log shows `EVT_SCHEDULE_OK` → receiver → notification
- [ ] **Exact alarm denied** → schedule; with app closed and device idle, WorkManager path eventually fires; UI shows "**Degraded timing (Doze)**" and logs `EVT_DOZE_FALLBACK_TAKEN`
- [ ] **Reboot device** with app closed → `BootReceiver` reschedules idempotently (UPSERT key), single notification posts at the next window
### Security
- [x] ~~All PendingIntents are immutable unless mutation is required~~ **RESOLVED**: Proper PendingIntent flags used
- [x] ~~Input validation on all @PluginMethod calls~~ **RESOLVED**: Error handler provides validation
- [x] ~~No hardcoded secrets or API keys~~ **RESOLVED**: Secure configuration management
- [x] ~~Secure network communication (HTTPS only)~~ **RESOLVED**: HTTPS enforcement in fetcher
- [x] ~~Proper permission handling~~ **RESOLVED**: `PermissionManager` exists
- [x] ~~All notification and alarm `PendingIntent`s use **`FLAG_IMMUTABLE`** unless mutation is required; if mutation is required, use `FLAG_UPDATE_CURRENT | FLAG_MUTABLE` with a stable `requestCode`~~ **RESOLVED**: Security guidelines implemented
### Documentation
- [ ] "How it Works" page with lifecycle diagrams
- [ ] Runbooks for common issues
- [ ] Complete API reference with schemas
- [ ] Error codes table with explanations
## Success Metrics
### Code Quality
- **Maintainability**: Reduced complexity in test UI
- **Testability**: Use-case classes with dependency injection
- **Reliability**: Improved error handling and logging
- **Security**: Input validation and network security
### User Experience
- **Clarity**: Clear status matrix with actionable items
- **Reliability**: Consistent notification delivery
- **Debugging**: Comprehensive diagnostics and logging
- **Performance**: Faster app startup and response
### Developer Experience
- **Documentation**: Complete API reference and runbooks
- **Testing**: Automated test coverage for critical paths
- **Debugging**: Structured logging and diagnostics
- **Maintenance**: Modular architecture for easy updates
## Conclusion
This implementation plan provides a structured approach to improving the DailyNotification Android test app. The plan focuses on architecture improvements, code organization, testing, and maintainability while maintaining the existing functionality.
The phased approach allows for incremental improvements while ensuring each phase delivers value. The acceptance criteria provide clear success metrics for each improvement area.
By following this plan, the test app will become more maintainable, reliable, and user-friendly while providing a solid foundation for future enhancements.
## Phase DoD (Definition of Done)
### Phase 1 DoD
- Status Matrix renders 5 fields: postNotifications, exactAlarms, channelEnabled, batteryOptIgnored, canScheduleNow.
- Input schema rejects bad `time`, long `title/body`, wrong `priority`.
- Boot reschedule idempotent (no dup rows, migration fence).
- @PluginMethod bodies ≤ 25 LOC, delegate to use-cases.
- "Copy Diagnostics (JSON)" button functional.
**Diagnostics MUST include:** appId, versionName/code, manufacturer/model, API level, timezone, `capacitor.config.json` plugin section echo, five status fields, last 50 event IDs, `webDir` effective path echo, `isDeviceIdleMode` boolean, `MAX_RESPONSE_SIZE` config value, currently selected `channelId`, `importance`, and `areNotificationsEnabled()` result.
- If exact alarm is denied/quota-limited, UI surfaces **"Degraded timing (Doze)"** and logs `EVT_DOZE_FALLBACK_TAKEN`.
### Phase 2 DoD
- Test UI split into modular scenarios with fixtures.
- Instrumentation tests cover channel disabled and exact alarm denied paths.
- Room migration test: `fallbackToDestructiveMigration(false)` + migration present and app boots
- Structured logging with event IDs for all operations.
- Error handling returns canonical error codes.
### Phase 3 DoD
- Security hardening implemented (HTTPS enforcement, input validation).
- Performance optimizations deployed (lazy loading, backoff strategy).
- Complete documentation with runbooks and API reference.
- Diagnostics system operational with health checks.
## RACI
- Owner: Android plugin maintainer
- Review: Mobile lead
- Consult: QA (instrumented tests), Web (bridge TS)
- Inform: Docs
## PR Checklist (copy/paste into PR template)
- [ ] Input validated at bridge boundary (unit tested)
- [ ] Use-case class created/updated (no logic in @PluginMethod)
- [ ] Logs include event IDs (start/finish/error)
- [ ] Status Matrix field(s) updated if capability changed
- [ ] Runbooks section touched if behavior changed
- [ ] No new events without ID (keeps logs grep-able)
- [ ] AndroidManifest receivers reviewed: only BootReceiver is exported; others remain `exported="false"`.
- [ ] CI lint script validates event IDs: greps for `Log.` calls and fails if unlisted event ID appears
## Test Matrix
| Scenario | Method(s) | Setup | Expected |
|---|---|---|
| Immediate notify | scheduleDailyNotification | Channel ON, perms granted | Success + toast seen |
| Channel disabled path | isChannelEnabled/openChannelSettings | Disable channel | Canonical `E_CHANNEL_DISABLED` |
| Exact alarm denied path | openExactAlarmSettings | Revoke exact alarm | Fallback path taken; logged `EVT_DOZE_FALLBACK_TAKEN` |
| Boot reschedule | BootReceiver | Reboot emulator | One (not duplicate) schedule restored |
| Doze idle window | scheduleDailyNotification | Device in idle | Fallback path taken; logged `EVT_DOZE_FALLBACK_TAKEN`; no crash |
| Bad schema rejects | bridge.ts + schema-validation.ts | Add unknown key / oversize title | Canonical `E_BAD_CONFIG` with single joined message |
| Timezone change | TimeChangeReceiver | Change device timezone | One rehydrated schedule, no duplicates |
| Manual clock skew | TimeChangeReceiver | Move clock +10m (no timezone) | Rescheduler recompute without duplicates; status remains green |
| Missing small icon | scheduleDailyNotification | No small icon set | Canonical error or logged warning; no silent drop |
| Closed app delivery | scheduleDailyNotification + DailyNotificationReceiver | App closed, screen off | Exact alarm delivers via receiver; log shows `EVT_SCHEDULE_OK` → receiver → notification |
| Closed app fallback | scheduleDailyNotification + WorkManager | App closed, device idle, exact alarm denied | WorkManager fires eventually; UI shows "Degraded timing (Doze)" |
| Closed app reboot | BootReceiver | App closed, device reboot | Single notification posts at next window; UPSERT prevents duplicates |
| Force-stopped app | scheduleDailyNotification + app launch | Force-stop from Settings, then launch | No delivery until next explicit launch; on launch, rescheduler restores future schedules; matrix shows advisory |
## Error Codes (canonical)
| Code | Meaning | Hint |
|---|---|---|
| E_INVALID_TIME | HH:mm invalid | Use 24h HH:mm |
| E_TITLE_TOO_LONG | >100 chars | Trim title |
| E_BODY_TOO_LONG | >500 chars | Trim body |
| E_PERMISSION_DENIED | Missing POST_NOTIFICATIONS | Request permission |
| E_CHANNEL_DISABLED | Channel blocked/low importance | Open channel settings |
| E_EXACT_ALARM_DENIED | No exact alarm | Open exact alarm settings / fallback |
| E_DOZE_LIMIT | Throttled by Doze | Expect delays; fallback taken |
| E_CHANNEL_MISSING | Channel ID not found at runtime | Recreate channel; verify ID & importance |
| E_BAD_CONFIG | Missing/invalid plugin config at startup | Check `capacitor.config.json` and diagnostics dump |
## Runbooks
### No notifications fire
Checks → Fix → Verify (matrix first, then perms, channel, exact alarm).
### Duplicates after reboot
Check storage for orphan schedule rows; verify idempotent rescheduler logs.
### Silent notifications
Verify channel importance and OEM-specific "Heads-up" settings.
### Preflight Golden Path (Demo)
1) Open app → run "Comprehensive Status" → all five fields green.
2) Tap "Open Channel Settings" → ensure importance = High.
3) Tap "Open Exact Alarm Settings" → grant if available.
4) Run "Immediate Notification" → toast & notif appear within 5s.
5) Schedule HH:mm+5 → lock screen → delivery within ±1m (exact) or delayed (fallback).