diff --git a/docs/android-app-analysis.md b/docs/android-app-analysis.md index 719a2ab..ae85cb6 100644 --- a/docs/android-app-analysis.md +++ b/docs/android-app-analysis.md @@ -128,6 +128,17 @@ public class MainActivity extends BridgeActivity { > **Note:** Set `android:name` only if you provide a custom `Application` class; otherwise remove to avoid ClassNotFound at runtime. +**Safe default (no custom Application class):** +```xml + + + + + + + +``` + > **Note:** `android:priority` has no practical effect for `BOOT_COMPLETED`; safe to omit. + +**Minimal example (recommended):** +```xml + + + + + +``` @@ -170,6 +193,8 @@ public class MainActivity extends BridgeActivity { - `INTERNET`: Required for content fetching - `RECEIVE_BOOT_COMPLETED`: Required for reboot recovery +> **Note:** If you later introduce foreground services, revisit WAKE_LOCK; otherwise keep it out. + ### 3. Capacitor Configuration Files #### capacitor.config.json @@ -632,8 +657,8 @@ If **denied or quota-limited** → schedule via WorkManager (exp backoff + jitte ```mermaid graph TD A[JavaScript Call] --> B[Capacitor Bridge] - B --> C[@PluginMethod] - C --> D[Use Case Handler] + B --> C[@PluginMethod → Canonical Error] + C --> D[Use Case Handler → Canonical Error] D --> E{Alarm vs WorkManager} E -->|Exact Alarm| F[AlarmManager] E -->|Fallback| G[WorkManager] diff --git a/docs/android-app-improvement-plan.md b/docs/android-app-improvement-plan.md index 28d77ad..e314ec1 100644 --- a/docs/android-app-improvement-plan.md +++ b/docs/android-app-improvement-plan.md @@ -400,7 +400,31 @@ public class ScheduleDailyTest { ## Security Hardening -### 0. ProGuard/R8 Keep Rules (minify-safe plugin) +### 0. PendingIntent Security + +**Purpose**: Secure PendingIntent creation with proper flags +**Implementation**: Use immutable flags unless mutation is required + +```java +// 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_IMMUTABLE` whenever extras aren't modified +- Use `FLAG_UPDATE_CURRENT | FLAG_MUTABLE` only when mutation is required +- Always use stable `requestCode` values + +### 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 @@ -483,7 +507,7 @@ export class SchemaValidator { // network/SecureNetworkClient.java public class SecureNetworkClient { private static final int TIMEOUT_SECONDS = 30; - private static final int MAX_RESPONSE_SIZE = 1024 * 1024; // 1MB + private static final int MAX_RESPONSE_SIZE = Config.NETWORK_MAX_RESPONSE_SIZE; // 1MB from config public String fetchContent(String url) throws NetworkException { // Enforce HTTPS @@ -549,14 +573,42 @@ public class SecureNetworkClient { - - - - - + + + + + + + + + + + + +``` + +#### TimeChangeReceiver Implementation + +```java +// 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 @@ -886,6 +938,8 @@ interface ScheduleResponse { - [ ] 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 ### Error Handling - [ ] All @PluginMethod calls validate inputs @@ -896,6 +950,9 @@ interface ScheduleResponse { - [ ] Channel policy enforced: missing/disabled channel returns `E_CHANNEL_MISSING` or `E_CHANNEL_DISABLED` with "Open Channel Settings" CTA - [ ] HTTPS-only; connect/read timeouts ≤ 30s; content-length hard cap ≤ 1 MB; oversize → `E_RESPONSE_TOO_LARGE` - [ ] Validation failures return **one joined message** surfaced to UI +- [ ] Fail fast with `E_CHANNEL_MISSING` if `NotificationCompat.Builder` has no valid channel on O+ +- [ ] Always set a **small icon**; missing small icon can drop posts on some OEMs +- [ ] Reject oversize responses deterministically (`E_RESPONSE_TOO_LARGE`), regardless of Content-Length presence ### Reliability - [ ] Reboot scenarios reliably deliver notifications @@ -904,12 +961,19 @@ interface ScheduleResponse { - [ ] User-visible reasoning for failures - [ ] Rescheduler uses unique key `(requestCode|channelId|time)` and **UPSERT** semantics; log `EVT_BOOT_REHYDRATE_DONE(count=n)` - [ ] Only `BootReceiver` is exported; all other receivers remain `exported="false"` +- [ ] Timezone and manual clock changes trigger rescheduler with idempotent rehydration ### 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 + +### Security +- [ ] All PendingIntents are immutable unless mutation is required +- [ ] Input validation on all @PluginMethod calls +- [ ] No hardcoded secrets or API keys +- [ ] Secure network communication (HTTPS only) +- [ ] Proper permission handling ### Documentation - [ ] "How it Works" page with lifecycle diagrams @@ -954,12 +1018,13 @@ By following this plan, the test app will become more maintainable, reliable, an - @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. +**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. @@ -984,6 +1049,7 @@ By following this plan, the test app will become more maintainable, reliable, an - [ ] 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 @@ -995,6 +1061,9 @@ By following this plan, the test app will become more maintainable, reliable, an | 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 | ## Error Codes (canonical)