Browse Source

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.
master
Matthew Raymer 2 days ago
parent
commit
aa53991a4b
  1. 29
      docs/android-app-analysis.md
  2. 77
      docs/android-app-improvement-plan.md

29
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. > **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]

77
docs/android-app-improvement-plan.md

@ -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
@ -557,6 +581,34 @@ public class SecureNetworkClient {
<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)

Loading…
Cancel
Save