docs: apply tight delta edits for correctness, resilience, and reviewer clarity
Implementation plan upgrades: - Add timezone & manual clock change resilience: TimeChangeReceiver with TIME_SET/TIMEZONE_CHANGED - Codify PendingIntent flags security: FLAG_IMMUTABLE vs FLAG_MUTABLE with examples - Add notification posting invariants: channel validation and small icon requirements - Clarify battery optimization UX limits: no ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS prompt - Move MAX_RESPONSE_SIZE to config: Config.NETWORK_MAX_RESPONSE_SIZE with diagnostics inclusion - Make Room migrations testable: fallbackToDestructiveMigration(false) test requirement - Enforce event IDs in PR checklist: CI lint script validation for Log. calls - Make degraded mode UI unmissable: visual badge + one-tap link to exact-alarm settings - Add channelId snapshot to diagnostics: include channelId, importance, areNotificationsEnabled() - Add manual clock skew test case: +10m clock move without timezone change Analysis doc correctness polish: - Add safe Application class example: show minimal <application> without android:name - Show minimal BOOT_COMPLETED example: remove android:priority attribute - Tighten WAKE_LOCK guidance: revisit only if introducing foreground services - Mirror Cordova guard in Build Config: already present (no change needed) - Add error surfaces to Mermaid flow: annotate @PluginMethod and Use Case Handler with → Canonical Error All changes maintain existing structure with surgical precision edits for correctness, resilience, and reviewer clarity.
This commit is contained in:
@@ -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.
|
> **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
|
||||||
|
<application>
|
||||||
|
<activity android:name=".MainActivity" android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
```
|
||||||
|
|
||||||
<!-- Plugin Components -->
|
<!-- Plugin Components -->
|
||||||
<!-- Internal receiver: keep non-exported unless intentionally public -->
|
<!-- Internal receiver: keep non-exported unless intentionally public -->
|
||||||
<receiver
|
<receiver
|
||||||
@@ -149,6 +160,18 @@ public class MainActivity extends BridgeActivity {
|
|||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
> **Note:** `android:priority` has no practical effect for `BOOT_COMPLETED`; safe to omit.
|
> **Note:** `android:priority` has no practical effect for `BOOT_COMPLETED`; safe to omit.
|
||||||
|
|
||||||
|
**Minimal example (recommended):**
|
||||||
|
```xml
|
||||||
|
<receiver
|
||||||
|
android:name="com.timesafari.dailynotification.BootReceiver"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
```
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<!-- Required Permissions -->
|
<!-- Required Permissions -->
|
||||||
@@ -170,6 +193,8 @@ public class MainActivity extends BridgeActivity {
|
|||||||
- `INTERNET`: Required for content fetching
|
- `INTERNET`: Required for content fetching
|
||||||
- `RECEIVE_BOOT_COMPLETED`: Required for reboot recovery
|
- `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
|
### 3. Capacitor Configuration Files
|
||||||
|
|
||||||
#### capacitor.config.json
|
#### capacitor.config.json
|
||||||
@@ -632,8 +657,8 @@ If **denied or quota-limited** → schedule via WorkManager (exp backoff + jitte
|
|||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
A[JavaScript Call] --> B[Capacitor Bridge]
|
A[JavaScript Call] --> B[Capacitor Bridge]
|
||||||
B --> C[@PluginMethod]
|
B --> C[@PluginMethod → Canonical Error]
|
||||||
C --> D[Use Case Handler]
|
C --> D[Use Case Handler → Canonical Error]
|
||||||
D --> E{Alarm vs WorkManager}
|
D --> E{Alarm vs WorkManager}
|
||||||
E -->|Exact Alarm| F[AlarmManager]
|
E -->|Exact Alarm| F[AlarmManager]
|
||||||
E -->|Fallback| G[WorkManager]
|
E -->|Fallback| G[WorkManager]
|
||||||
|
|||||||
@@ -400,7 +400,31 @@ public class ScheduleDailyTest {
|
|||||||
|
|
||||||
## Security Hardening
|
## 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
|
**Purpose**: Prevent Capacitor annotations and plugin methods from being stripped
|
||||||
**Implementation**: Add keep rules to proguard-rules.pro
|
**Implementation**: Add keep rules to proguard-rules.pro
|
||||||
@@ -483,7 +507,7 @@ export class SchemaValidator {
|
|||||||
// network/SecureNetworkClient.java
|
// network/SecureNetworkClient.java
|
||||||
public class SecureNetworkClient {
|
public class SecureNetworkClient {
|
||||||
private static final int TIMEOUT_SECONDS = 30;
|
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 {
|
public String fetchContent(String url) throws NetworkException {
|
||||||
// Enforce HTTPS
|
// Enforce HTTPS
|
||||||
@@ -549,14 +573,42 @@ public class SecureNetworkClient {
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name="com.timesafari.dailynotification.BootReceiver"
|
android:name="com.timesafari.dailynotification.BootReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter android:priority="1000">
|
<intent-filter android:priority="1000">
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</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
|
||||||
|
|
||||||
|
```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
|
#### Key Features
|
||||||
@@ -886,6 +938,8 @@ interface ScheduleResponse {
|
|||||||
- [ ] Provides actionable buttons for issues
|
- [ ] Provides actionable buttons for issues
|
||||||
- [ ] Exports diagnostics as JSON
|
- [ ] Exports diagnostics as JSON
|
||||||
- [ ] When fallback is active, matrix shows **"Degraded timing (Doze)"** and last event includes `EVT_DOZE_FALLBACK_TAKEN`
|
- [ ] 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
|
### Error Handling
|
||||||
- [ ] All @PluginMethod calls validate inputs
|
- [ ] 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
|
- [ ] 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`
|
- [ ] 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
|
- [ ] 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
|
### Reliability
|
||||||
- [ ] Reboot scenarios reliably deliver notifications
|
- [ ] Reboot scenarios reliably deliver notifications
|
||||||
@@ -904,12 +961,19 @@ interface ScheduleResponse {
|
|||||||
- [ ] User-visible reasoning for failures
|
- [ ] User-visible reasoning for failures
|
||||||
- [ ] Rescheduler uses unique key `(requestCode|channelId|time)` and **UPSERT** semantics; log `EVT_BOOT_REHYDRATE_DONE(count=n)`
|
- [ ] 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"`
|
- [ ] Only `BootReceiver` is exported; all other receivers remain `exported="false"`
|
||||||
|
- [ ] Timezone and manual clock changes trigger rescheduler with idempotent rehydration
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
- [ ] Test UI modularized into scenarios
|
- [ ] Test UI modularized into scenarios
|
||||||
- [ ] At least 2 scenarios run as automated tests
|
- [ ] At least 2 scenarios run as automated tests
|
||||||
- [ ] Instrumentation tests cover critical paths
|
- [ ] 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
|
### Documentation
|
||||||
- [ ] "How it Works" page with lifecycle diagrams
|
- [ ] "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.
|
- @PluginMethod bodies ≤ 25 LOC, delegate to use-cases.
|
||||||
- "Copy Diagnostics (JSON)" button functional.
|
- "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`.
|
- If exact alarm is denied/quota-limited, UI surfaces **"Degraded timing (Doze)"** and logs `EVT_DOZE_FALLBACK_TAKEN`.
|
||||||
|
|
||||||
### Phase 2 DoD
|
### Phase 2 DoD
|
||||||
- Test UI split into modular scenarios with fixtures.
|
- Test UI split into modular scenarios with fixtures.
|
||||||
- Instrumentation tests cover channel disabled and exact alarm denied paths.
|
- 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.
|
- Structured logging with event IDs for all operations.
|
||||||
- Error handling returns canonical error codes.
|
- 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
|
- [ ] Runbooks section touched if behavior changed
|
||||||
- [ ] No new events without ID (keeps logs grep-able)
|
- [ ] No new events without ID (keeps logs grep-able)
|
||||||
- [ ] AndroidManifest receivers reviewed: only BootReceiver is exported; others remain `exported="false"`.
|
- [ ] 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
|
## 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 |
|
| 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 |
|
| 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 |
|
| 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)
|
## Error Codes (canonical)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user