26 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 module
- Add input schema validation
- Centralize exact-alarm gate
- Make BootReceiver idempotent
- 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 improvements
- Add structured logging
Phase 3: Security & Performance
Focus: Security hardening and performance optimization
- Implement security hardening
- Add performance optimizations
- Create diagnostics system
- 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
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
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
if (request.title && request.title.length > 100) {
errors.push('Title must be 100 characters or less');
}
// Validate body length
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');
}
return {
isValid: errors.length === 0,
errors
};
}
}
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 = 1024 * 1024; // 1MB
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
long contentLength = connection.getContentLengthLong();
if (contentLength > MAX_RESPONSE_SIZE) {
throw new NetworkException("E_RESPONSE_TOO_LARGE", "Response too large");
}
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 -->
<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"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
Key Features
- Export Control: Minimize exported components
- 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
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
- [ ] **Status Matrix Module**
- Implement `collectRuntimeStatus()` function
- Create status matrix UI component
- Add "Copy Diagnostics" functionality
- [ ] **Input Schema Validation**
- Create TypeScript schema definitions
- Implement validation at bridge boundary
- Add error handling for validation failures
- [ ] **Exact-Alarm Gate**
- Create `ExactAlarmManager` class
- Implement graceful fallback logic
- Update status matrix to show exact alarm status
- [ ] **BootReceiver Idempotent**
- Add migration fence for old schedules
- Implement idempotent rescheduling
- Add logging for boot recovery
- [ ] **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
- [ ] **Structured Logging**
- Add event IDs for all operations
- Implement progress logging
- Create log export functionality
### Phase 3: Security & Performance
- [ ] **Security Hardening**
- Add network security measures
- Review intent filter security
- Implement channel policy enforcement
- [ ] **Performance Optimizations**
- Implement lazy loading for UI modules
- Add worker backoff strategy
- Optimize database operations
- [ ] **Diagnostics System**
- Implement comprehensive diagnostics
- Add performance monitoring
- Create health check endpoints
- [ ] **Documentation Updates**
- Create "How it Works" documentation
- Write runbooks for common issues
- Complete API reference
## Acceptance Criteria
### Status Matrix
- [ ] Reports all relevant runtime capabilities
- [ ] Shows live channel state
- [ ] Provides actionable buttons for issues
- [ ] Exports diagnostics as JSON
### Error Handling
- [ ] All @PluginMethod calls validate inputs
- [ ] Returns stable error codes with hints
- [ ] Maps native exceptions to canonical errors
- [ ] Provides user-friendly error messages
### Reliability
- [ ] Reboot scenarios reliably deliver notifications
- [ ] Doze scenarios degrade gracefully
- [ ] Clear logs explain system behavior
- [ ] User-visible reasoning for failures
### Testing
- [ ] Test UI modularized into scenarios
- [ ] At least 2 scenarios run as automated tests
- [ ] Instrumentation tests cover critical paths
- [ ] Unit tests cover use-case classes
### 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.