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)