feat(android): Capacitor APK wrapper + on-demand CI build #156

Merged
forgejo_admin merged 13 commits from feat/android-apk-issue-153 into master 2026-05-17 12:15:23 +00:00

Closes #153.

What

  • Capacitor v7 scaffold producing a sideloadable debug APK from the existing dist/web/ bundle.
  • .github/workflows/build-android.yml — manually-triggered (workflow_dispatch only) CI workflow that builds the APK and uploads it as a workflow artifact.
  • Docs updated with Q1–Q5 decisions; PRIVACY_GAPS.md updated with the Android ≤ 9 Downloads/ world-readable gap.
  • Implementation plan at docs/superpowers/plans/2026-05-17-android-apk-and-ci.md.

Decisions (Q1–Q5 from issue #153)

Q Decision
Q1 — file output Accept system Downloads/ for v1; documented as a privacy gap on Android ≤ 9 (PRIVACY_GAPS.md).
Q2 — back button Capacitor default (close on root).
Q3 — android/ placement Committed to master.
Q4 — CI In scope; workflow_dispatch only — per founder direction: "started manually not on merge or PR."
Q5 — min API 23 (Capacitor 7 default).

How to test the CI workflow

  1. Merge this PR.
  2. From the Forgejo Actions UI: Actions → Build Android APK → Run workflow against master.
  3. Download the metascrub-debug-apk artifact (~5–10 MB).
  4. Sideload onto a real Android device; verify file picker works, JPEG/PNG/PDF strip works, cleaned file lands in Downloads/.
  5. Verify no network traffic during processing (mitmproxy or platform-side firewall).

Local commands (require JDK 17 + Android SDK installed)

yarn android:sync     # build dist/web/ and copy into android/app/src/main/assets/public/
yarn android:build    # the above + ./gradlew assembleDebug

Privacy invariants

  • CSP unchanged (connect-src 'self' from vite.config.web.ts).
  • No telemetry, no auto-update, no analytics. npx cap init ran with telemetry declined.
  • server.androidScheme: 'https' set explicitly so WASM and service workers stay in a secure context.
  • *.jks/*.keystore/app/release/ added to android/.gitignore (was commented out in the Capacitor template).

Quality gates

  • yarn typecheck
  • yarn lint
  • yarn test ✓ (509/509)
  • yarn check:deps ✓ (no circular deps)
  • Local APK build not verified (no JDK/Android SDK on this machine — CI is the verification path).

Out of scope (deferred)

  • Release-signed APK (needs keystore as CI secret).
  • @capacitor/filesystem adapter for the Android ≤ 9 Downloads/ mitigation (documented as a follow-up in PRIVACY_GAPS.md).
  • F-Droid submission.
  • iOS (npx cap add ios).
Closes #153. ## What - Capacitor v7 scaffold producing a sideloadable debug APK from the existing `dist/web/` bundle. - `.github/workflows/build-android.yml` — manually-triggered (`workflow_dispatch` only) CI workflow that builds the APK and uploads it as a workflow artifact. - Docs updated with Q1–Q5 decisions; `PRIVACY_GAPS.md` updated with the Android ≤ 9 `Downloads/` world-readable gap. - Implementation plan at `docs/superpowers/plans/2026-05-17-android-apk-and-ci.md`. ## Decisions (Q1–Q5 from issue #153) | Q | Decision | | --- | --- | | Q1 — file output | Accept system `Downloads/` for v1; documented as a privacy gap on Android ≤ 9 (`PRIVACY_GAPS.md`). | | Q2 — back button | Capacitor default (close on root). | | Q3 — `android/` placement | Committed to `master`. | | Q4 — CI | In scope; `workflow_dispatch` only — per founder direction: *"started manually not on merge or PR."* | | Q5 — min API | 23 (Capacitor 7 default). | ## How to test the CI workflow 1. Merge this PR. 2. From the Forgejo Actions UI: **Actions → Build Android APK → Run workflow** against `master`. 3. Download the `metascrub-debug-apk` artifact (~5–10 MB). 4. Sideload onto a real Android device; verify file picker works, JPEG/PNG/PDF strip works, cleaned file lands in `Downloads/`. 5. Verify no network traffic during processing (mitmproxy or platform-side firewall). ## Local commands (require JDK 17 + Android SDK installed) ```bash yarn android:sync # build dist/web/ and copy into android/app/src/main/assets/public/ yarn android:build # the above + ./gradlew assembleDebug ``` ## Privacy invariants - CSP unchanged (`connect-src 'self'` from `vite.config.web.ts`). - No telemetry, no auto-update, no analytics. `npx cap init` ran with telemetry declined. - `server.androidScheme: 'https'` set explicitly so WASM and service workers stay in a secure context. - `*.jks`/`*.keystore`/`app/release/` added to `android/.gitignore` (was commented out in the Capacitor template). ## Quality gates - `yarn typecheck` ✓ - `yarn lint` ✓ - `yarn test` ✓ (509/509) - `yarn check:deps` ✓ (no circular deps) - Local APK build not verified (no JDK/Android SDK on this machine — CI is the verification path). ## Out of scope (deferred) - Release-signed APK (needs keystore as CI secret). - `@capacitor/filesystem` adapter for the Android ≤ 9 `Downloads/` mitigation (documented as a follow-up in `PRIVACY_GAPS.md`). - F-Droid submission. - iOS (`npx cap add ios`).
forgejo_admin added 8 commits 2026-05-17 10:14:35 +00:00
- Capacitor 7 generated android/ Gradle project
- minSdk 23 / compileSdk 35 / targetSdk 35 (per variables.gradle)
- App id: com.metascrub.app; display name: MetaScrub
- versionCode 1 / versionName 4.0.0 (matches package.json)
- .gitignore excludes .gradle/, build/, .idea/, local.properties,
  and uncommented *.jks/*.keystore/app/release/ for keystore safety
Manual trigger only — per issue #153, the action is run on demand
rather than on each merge/PR. Builds dist/web/, syncs into Capacitor,
runs gradlew assembleDebug, uploads app-debug.apk as a workflow artifact.
docs(privacy): document Android Downloads/ world-readable gap on Android <=9 (refs #153)
All checks were successful
CI / Lint, Typecheck & Unit Tests (pull_request) Successful in 19s
CI / E2E (Standalone single-file) (pull_request) Successful in 3m45s
CI / E2E (Web) (pull_request) Successful in 5m45s
f7a16a160d
forgejo_admin added 1 commit 2026-05-17 11:39:16 +00:00
ci(android): use forgejo-stack/job-android container (refs #153)
All checks were successful
CI / Lint, Typecheck & Unit Tests (pull_request) Successful in 26s
CI / E2E (Standalone single-file) (pull_request) Successful in 2m9s
CI / E2E (Web) (pull_request) Successful in 3m14s
5afe8fd421
Drops ~3-5 min cold-build time by using the Android-augmented job image
introduced in forgejo-stack#1. JDK 17 + Android SDK (platforms;android-35
+ build-tools;35.0.0) are baked into the container, so setup-java@v4 and
android-actions/setup-android@v3 are no longer needed.

New cold-build time estimate:
  ~3-5 min  (Gradle deps + assembleDebug + upload)
Versus the previous workflow:
  ~10-15 min cold / ~3-5 min warm (cached SDK + Gradle)
forgejo_admin added 1 commit 2026-05-17 11:52:31 +00:00
ci(android): make pre-baked image a soft dependency via workflow input (refs #153)
All checks were successful
CI / Lint, Typecheck & Unit Tests (pull_request) Successful in 25s
CI / E2E (Standalone single-file) (pull_request) Successful in 1m10s
CI / E2E (Web) (pull_request) Successful in 2m13s
1a96af7644
Hard `container: forgejo-stack/job-android:latest` coupled the workflow
to forgejo-stack infra — any runner without that image would fail
immediately. Add a `use_prebaked_image` workflow_dispatch input:

- true (default): use forgejo-stack/job-android:latest, skip setup-java
  and setup-android steps. Cold build ~3-5 min.
- false: run on runner default image, install JDK 17 + Android SDK at
  runtime via setup-java@v4 + android-actions/setup-android@v3.
  Cold build ~10-15 min.

Workflow file remains portable across runner setups (GitHub-hosted,
vanilla act_runner, forgejo-stack with or without the optional Android
image). Documented in docs/android-apk.md \xC2\xA7CI.
Author
Owner

Code review — PR #156

Reviewing as a critical reader. Confidence in parentheses; surfacing anything > 50% per project review threshold.

P0 — should address before merge

1. (90%) INTERNET permission needs to be documented as a privacy invariant gap.

android/app/src/main/AndroidManifest.xml:40 declares <uses-permission android:name="android.permission.INTERNET" />. Capacitor needs this for the https://localhost/ WebView scheme — even local-served bundled assets require the platform INTERNET grant. The CSP connect-src 'self' blocks remote calls at the WebView layer, but anyone auditing the APK with aapt dump permissions will see this and ask. Privacy invariant §5 says "zero outbound network traffic in production"; the manifest will appear to contradict that.

Fix: add a subsection to docs/PRIVACY_GAPS.md explaining (a) why INTERNET is in the manifest, (b) what enforces the actual no-traffic guarantee (CSP + no analytics/fetch sites), (c) reference the no-network Playwright test (issue #67) as the operational verification path. Don't try to remove the permission — it's load-bearing for Capacitor's scheme handler.

2. (85%) android:allowBackup="true" is a real exposure.

AndroidManifest.xml:5 defaults to allowing adb backup and Google Drive auto-backup of the app's private storage. For a privacy-focused tool that may briefly cache files mid-processing, this leaks state via a vector the user doesn't expect. Set android:allowBackup="false" in <application> — one-line fix.

3. (75%) Workflow container: expression may not evaluate correctly.

.github/workflows/build-android.yml:38 uses:

container: ${{ inputs.use_prebaked_image && 'forgejo-stack/job-android:latest' || '' }}

GitHub Actions converts workflow_dispatch boolean inputs to strings ('true' / 'false') at the YAML expression layer, and both strings are truthy under &&. Forgejo's act_runner may differ but the safe form is:

container: ${{ inputs.use_prebaked_image == true && 'forgejo-stack/job-android:latest' || '' }}

Verify on first dispatch — if the fallback path gets used unexpectedly, that's why.

P1 — should address; not blockers

4. (90%) Launcher icons are generic Capacitor placeholders, not MetaScrub.

android/app/src/main/res/mipmap-*/ic_launcher.png are the Capacitor Android template defaults (verified: diff against .resources/icon.png returns differ). On install, the home-screen icon is the generic Android-bot, not the MetaScrub icon shipped in .resources/. App label is correct (MetaScrub). Generate proper launcher icons from .resources/icon.png (or static/icon.svg) via Android Studio's Image Asset wizard or a CLI tool.

5. (80%) Google Services plugin reference is dead code in an app that shouldn't go anywhere near Google infra.

android/app/build.gradle:47-54:

try {
  def servicesJSON = file('google-services.json')
  if (servicesJSON.text) {
    apply plugin: 'com.google.gms.google-services'
  }
}

Gated behind a file-exists check, but the plugin reference is in the build script. For an app whose project-direction.md explicitly says no Google dependency, delete the whole try-catch. F-Droid reviewers will flag this; paranoid security auditors will too.

6. (70%) Splash screen is Capacitor's default gradient.

android/app/src/main/res/drawable-{land,port}-*/splash.png are unbranded gradients. App-launch users will see a Capacitor-flavoured splash flash before the WebView paints. Not a blocker, but worth a follow-up to replace with the MetaScrub icon on a solid bg.

7. (65%) FileProvider paths are wider than needed.

android/app/src/main/res/xml/file_paths.xml:

<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />

path="." exposes every file under each base path via FileProvider URIs. Mitigated by android:exported="false" on the provider, but the surface area is broader than necessary. MetaScrub doesn't appear to use FileProvider directly (<a download> routes through DownloadManager). Either tighten the paths to the specific subdirs we actually share, or remove the provider stanza entirely. If a future Capacitor plugin needs FileProvider, add it back narrowly then.

P2 — follow-ups, not for this PR

8. (60%) Version skew: package.json is 4.0.0, CLAUDE.md / README reference v5.

I matched versionName "4.0.0" in android/app/build.gradle to package.json. If the v5 rebrand is real, package.json needs a bump first, then Android version follows. Separate housekeeping issue.

9. (55%) No NOTICE / THIRD_PARTY_LICENSES for the new transitive Apache-2.0 deps.

Capacitor + Cordova plugins are Apache-2.0. The repo doesn't aggregate license notices. Needed before F-Droid submission. Out of scope here; track as F-Droid prep.

10. (50%) Fallback path's runner-image floor isn't documented.

use_prebaked_image=false assumes the runner provides Node 22 + yarn + apt. Forgejo-stack default, GitHub-hosted, and standard act_runner configs all qualify. A bare node:slim does not. Worth one line in docs/android-apk.md §CI: "the fallback path requires Node 22 + apt on the runner; verified against forgejo-stack/job:latest and GitHub-hosted ubuntu-latest."

Things I checked and they're fine

  • MainActivity.java is the Capacitor default BridgeActivity — no custom code paths to audit.
  • capacitor.config.ts correctly sets server.androidScheme: 'https'.
  • No JPEG/PNG/PDF/etc. processing code changed — the strategy registry is untouched.
  • The CSP meta tag in vite.config.web.ts is unchanged.
  • .gitignore correctly excludes keystores (*.jks, *.keystore, app/release/).
  • package.json prod deps stayed at 4 (Capacitor went to devDependencies).
  • yarn check:deps (madge --circular) passes.
  • yarn test passes (509/509).
  • Plan + decisions doc is honest about what's in/out of scope.

Recommendation

P0 items #1 (PRIVACY_GAPS.md update) and #2 (allowBackup="false") before merge. #3 (expression syntax) can be verified on first dispatch. #5 (Google Services plugin deletion) is small enough to bundle with #1/#2. The rest are reasonable follow-up issues.

## Code review — PR #156 Reviewing as a critical reader. Confidence in parentheses; surfacing anything > 50% per project review threshold. ### P0 — should address before merge **1. (90%) `INTERNET` permission needs to be documented as a privacy invariant gap.** `android/app/src/main/AndroidManifest.xml:40` declares `<uses-permission android:name="android.permission.INTERNET" />`. Capacitor needs this for the `https://localhost/` WebView scheme — even local-served bundled assets require the platform INTERNET grant. The CSP `connect-src 'self'` blocks remote calls at the WebView layer, but anyone auditing the APK with `aapt dump permissions` will see this and ask. Privacy invariant §5 says "zero outbound network traffic in production"; the manifest will appear to contradict that. Fix: add a subsection to `docs/PRIVACY_GAPS.md` explaining (a) why INTERNET is in the manifest, (b) what enforces the actual no-traffic guarantee (CSP + no analytics/fetch sites), (c) reference the no-network Playwright test (issue #67) as the operational verification path. Don't try to remove the permission — it's load-bearing for Capacitor's scheme handler. **2. (85%) `android:allowBackup="true"` is a real exposure.** `AndroidManifest.xml:5` defaults to allowing `adb backup` and Google Drive auto-backup of the app's private storage. For a privacy-focused tool that may briefly cache files mid-processing, this leaks state via a vector the user doesn't expect. Set `android:allowBackup="false"` in `<application>` — one-line fix. **3. (75%) Workflow `container:` expression may not evaluate correctly.** `.github/workflows/build-android.yml:38` uses: ```yaml container: ${{ inputs.use_prebaked_image && 'forgejo-stack/job-android:latest' || '' }} ``` GitHub Actions converts `workflow_dispatch` boolean inputs to strings (`'true'` / `'false'`) at the YAML expression layer, and both strings are truthy under `&&`. Forgejo's act_runner may differ but the safe form is: ```yaml container: ${{ inputs.use_prebaked_image == true && 'forgejo-stack/job-android:latest' || '' }} ``` Verify on first dispatch — if the fallback path gets used unexpectedly, that's why. ### P1 — should address; not blockers **4. (90%) Launcher icons are generic Capacitor placeholders, not MetaScrub.** `android/app/src/main/res/mipmap-*/ic_launcher.png` are the Capacitor Android template defaults (verified: `diff` against `.resources/icon.png` returns differ). On install, the home-screen icon is the generic Android-bot, not the MetaScrub icon shipped in `.resources/`. App label is correct (`MetaScrub`). Generate proper launcher icons from `.resources/icon.png` (or `static/icon.svg`) via Android Studio's Image Asset wizard or a CLI tool. **5. (80%) Google Services plugin reference is dead code in an app that shouldn't go anywhere near Google infra.** `android/app/build.gradle:47-54`: ``` try { def servicesJSON = file('google-services.json') if (servicesJSON.text) { apply plugin: 'com.google.gms.google-services' } } ``` Gated behind a file-exists check, but the *plugin reference* is in the build script. For an app whose project-direction.md explicitly says no Google dependency, delete the whole try-catch. F-Droid reviewers will flag this; paranoid security auditors will too. **6. (70%) Splash screen is Capacitor's default gradient.** `android/app/src/main/res/drawable-{land,port}-*/splash.png` are unbranded gradients. App-launch users will see a Capacitor-flavoured splash flash before the WebView paints. Not a blocker, but worth a follow-up to replace with the MetaScrub icon on a solid bg. **7. (65%) FileProvider paths are wider than needed.** `android/app/src/main/res/xml/file_paths.xml`: ```xml <external-path name="my_images" path="." /> <cache-path name="my_cache_images" path="." /> ``` `path="."` exposes every file under each base path via FileProvider URIs. Mitigated by `android:exported="false"` on the provider, but the surface area is broader than necessary. MetaScrub doesn't appear to use FileProvider directly (`<a download>` routes through DownloadManager). Either tighten the paths to the specific subdirs we actually share, or remove the provider stanza entirely. If a future Capacitor plugin needs FileProvider, add it back narrowly then. ### P2 — follow-ups, not for this PR **8. (60%) Version skew: `package.json` is `4.0.0`, CLAUDE.md / README reference v5.** I matched `versionName "4.0.0"` in `android/app/build.gradle` to `package.json`. If the v5 rebrand is real, `package.json` needs a bump first, then Android version follows. Separate housekeeping issue. **9. (55%) No `NOTICE` / `THIRD_PARTY_LICENSES` for the new transitive Apache-2.0 deps.** Capacitor + Cordova plugins are Apache-2.0. The repo doesn't aggregate license notices. Needed before F-Droid submission. Out of scope here; track as F-Droid prep. **10. (50%) Fallback path's runner-image floor isn't documented.** `use_prebaked_image=false` assumes the runner provides Node 22 + yarn + apt. Forgejo-stack default, GitHub-hosted, and standard `act_runner` configs all qualify. A bare `node:slim` does not. Worth one line in `docs/android-apk.md` §CI: "the fallback path requires Node 22 + apt on the runner; verified against forgejo-stack/job:latest and GitHub-hosted ubuntu-latest." ### Things I checked and they're fine - `MainActivity.java` is the Capacitor default `BridgeActivity` — no custom code paths to audit. - `capacitor.config.ts` correctly sets `server.androidScheme: 'https'`. - No JPEG/PNG/PDF/etc. processing code changed — the strategy registry is untouched. - The CSP meta tag in `vite.config.web.ts` is unchanged. - `.gitignore` correctly excludes keystores (`*.jks`, `*.keystore`, `app/release/`). - `package.json` prod deps stayed at 4 (Capacitor went to devDependencies). - `yarn check:deps` (madge --circular) passes. - `yarn test` passes (509/509). - Plan + decisions doc is honest about what's in/out of scope. ### Recommendation P0 items #1 (PRIVACY_GAPS.md update) and #2 (`allowBackup="false"`) before merge. #3 (expression syntax) can be verified on first dispatch. #5 (Google Services plugin deletion) is small enough to bundle with #1/#2. The rest are reasonable follow-up issues.
forgejo_admin added 3 commits 2026-05-17 12:07:52 +00:00
P0 + P1 fixes from review #892 (P0 #1 + #2, P1 #5 + #7):

- AndroidManifest.xml: allowBackup=false + fullBackupContent=false +
  dataExtractionRules for Android 12+ — closes adb backup / Google Drive
  auto-backup / device-transfer exfiltration vectors. Add explicit
  usesCleartextTraffic=false. Inline-comment why INTERNET is granted.
- res/xml/data_extraction_rules.xml: explicit deny-all backup + transfer
  rules for Android 12+ (covers the gap not handled by allowBackup=false).
- Delete FileProvider stanza + res/xml/file_paths.xml — MetaScrub doesn't
  use FileProvider directly (downloads route through DownloadManager via
  <a download>). Remove unused surface area.
- android/app/build.gradle: delete the Capacitor-template try-catch that
  conditionally applies com.google.gms.google-services. The plugin
  reference is gone entirely so 'gradlew dependencies' and 'aapt dump'
  are clean for F-Droid + security audits.
- docs/PRIVACY_GAPS.md: new section 'Android INTERNET permission —
  granted but never used' explaining why Capacitor needs INTERNET, what
  enforces the actual no-traffic guarantee (CSP connect-src 'self'),
  and how to verify (aapt dump, host-side firewall, future #67).
P1 #4 + #6 from review #892.

- scripts/generate_android_assets.py: regenerates icons + splashes from
  .resources/icon.png (the 1024x1024 source). Pure-PIL, runs in-tree.
- mipmap-{mdpi,hdpi,xhdpi,xxhdpi,xxxhdpi}/ic_launcher.png — replaced
  Capacitor's placeholder Android-bot with MetaScrub icon (48/72/96/144/192).
- Round icons rendered with a circular alpha mask at the same densities.
- drawable-{port,land}-{mdpi..xxxhdpi}/splash.png — MetaScrub icon
  centered on #1a1a1a (matches the privacy-app dark aesthetic).
- Deleted Capacitor adaptive-icon machinery (mipmap-anydpi-v26/*.xml,
  drawable/ic_launcher_background.xml, drawable-v24/ic_launcher_foreground.xml,
  mipmap-*/ic_launcher_foreground.png, values/ic_launcher_background.xml).
  Android 8+ falls back to the legacy MetaScrub PNGs we just generated —
  fine for a v1 sideload target. Adaptive vector path can come later.
ci(android): fix boolean expression for use_prebaked_image input (refs #153)
All checks were successful
CI / Lint, Typecheck & Unit Tests (pull_request) Successful in 27s
CI / E2E (Standalone single-file) (pull_request) Successful in 1m15s
CI / E2E (Web) (pull_request) Successful in 2m23s
9f4526d726
P0 #3 from review #892.

workflow_dispatch boolean inputs are received as strings ('true'/'false')
in expression evaluation; both are truthy under bare `&&`, so:

    container: ${{ inputs.use_prebaked_image && 'X' || '' }}

would always pick 'X' regardless of the input. Use explicit
`== true` / `!= true` comparisons:

    container: ${{ inputs.use_prebaked_image == true && 'X' || '' }}
    if: ${{ inputs.use_prebaked_image != true }}
Author
Owner

Code review round 2 — PR #156

Verifying round 1's fixes + scanning for newly introduced issues.

Round 1 P0/P1 verification — all closed

Item Status
P0 #1 INTERNET permission documented docs/PRIVACY_GAPS.md has the new "Android INTERNET permission" section + inline comment in AndroidManifest.xml
P0 #2 allowBackup=false Belt-and-suspenders: allowBackup=false + fullBackupContent=false + dataExtractionRules deny-all for Android 12+. Also added usesCleartextTraffic=false
P0 #3 Boolean expression == true / != true applied at the three sites in build-android.yml
P1 #4 Branded launcher icons 5 densities × {legacy, round} regenerated from .resources/icon.png via scripts/generate_android_assets.py. Capacitor adaptive XMLs deleted
P1 #5 Google Services plugin Try-catch removed from build.gradle; explanatory comment in its place
P1 #6 Branded splash Icon on #1a1a1a at 5 densities × 2 orientations + the unqualified drawable/splash.png
P1 #7 FileProvider Stanza removed from manifest; res/xml/file_paths.xml deleted

New findings (round 2)

1. (75%) Android 12+'s Theme.SplashScreen API ignores android:background.

android/app/src/main/res/values/styles.xml:19-21:

<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
    <item name="android:background">@drawable/splash</item>
</style>

This is the Capacitor template's stock styling, but Theme.SplashScreen (the new Android-12+ splash API from androidx-core-splashscreen) doesn't honor android:background — it requires windowSplashScreenBackground (color) and windowSplashScreenAnimatedIcon (drawable, 432dp safe-zone). Practical impact:

  • Android 6–11: falls through to the legacy splash mechanism, shows our generated splash.png correctly.
  • Android 12+: uses the system splash with @mipmap/ic_launcher (our MetaScrub icon, which is good!) on a system-themed background (white in light mode, black in dark mode — NOT our #1a1a1a).

On Android 12+ the user still sees the MetaScrub icon, just not on our chosen background. Acceptable for v1 sideload; track as a polish follow-up. Full fix needs a 432dp foreground icon with a 240dp safe zone (so adaptive masking doesn't crop it) — scripts/generate_android_assets.py could produce this in a future pass.

2. (60%) dataExtractionRules is API 31+ only — tools:targetApi annotation would silence lint warnings.

With minSdk=23 and android:dataExtractionRules="@xml/data_extraction_rules", AGP's lint will emit a NewApi warning (attribute unsupported on API < 31). Behavior is correct — the attribute is silently ignored on older API levels, and allowBackup=false + fullBackupContent=false cover those. Cleanest fix:

<manifest xmlns:android="..." xmlns:tools="http://schemas.android.com/tools">
    <application ... tools:targetApi="31">

Doesn't change runtime behavior; just makes the lint output clean. Low priority.

3. (55%) Splash background #1a1a1a is arbitrary, not tied to the app's brand tokens.

src/web/styles/tokens.css has the canonical brand colors. The generator script hard-codes #1a1a1a near-black, which is close enough but isn't var(--background-dark) or whatever the actual token is. Worth aligning in a future pass.

Things I verified are NOT broken

  • styles.xml references @color/colorPrimary, colorPrimaryDark, colorAccent — these are not defined in our res/values/, but resolve from androidx.appcompat:appcompat:1.7.0's bundled resources. Capacitor 7 template intentionally ships no colors.xml; relies on appcompat. Verified no other code references the deleted ic_launcher_background color.
  • The deletion of mipmap-anydpi-v26/ + drawable-v24/ empty dirs is clean — manifest no longer references adaptive-icon resources.
  • npx cap sync android is unaffected — it copies dist/web/ into assets/public/, doesn't touch icons or manifest.
  • usesCleartextTraffic="false" doesn't break Capacitor's https://localhost/ (that scheme is intercepted by Capacitor's WebView, not Android's networking stack).
  • data_extraction_rules.xml is redundant with allowBackup=false (which already blocks everything) — but it's defense-in-depth and harmless.

Carry-over P2 (unchanged from round 1)

  • versionName 4.0.0 vs CLAUDE.md/README "v5" — package.json housekeeping
  • No NOTICE / THIRD_PARTY_LICENSES for the new Apache-2.0 deps — F-Droid prep
  • Fallback path runner floor (Node 22 + apt) not explicitly documented in §CI

Recommendation

Ready to merge. All P0 and P1 from round 1 closed. The three new round-2 findings are P2 polish — split into follow-up issues after merge:

  1. Android 12+ proper splash (windowSplashScreenBackground + animated foreground icon)
  2. lint cleanup (tools:targetApi annotations)
  3. Splash bg color from brand tokens

The quality gates (yarn typecheck, yarn lint, yarn test, yarn check:deps) are all green. The actual Gradle build remains unverified locally (no JDK on this host) — the CI workflow is the verification path post-merge.

## Code review round 2 — PR #156 Verifying round 1's fixes + scanning for newly introduced issues. ### Round 1 P0/P1 verification — all closed | Item | Status | | --- | --- | | P0 #1 INTERNET permission documented | ✅ `docs/PRIVACY_GAPS.md` has the new "Android INTERNET permission" section + inline comment in `AndroidManifest.xml` | | P0 #2 allowBackup=false | ✅ Belt-and-suspenders: `allowBackup=false` + `fullBackupContent=false` + `dataExtractionRules` deny-all for Android 12+. Also added `usesCleartextTraffic=false` | | P0 #3 Boolean expression | ✅ `== true` / `!= true` applied at the three sites in `build-android.yml` | | P1 #4 Branded launcher icons | ✅ 5 densities × {legacy, round} regenerated from `.resources/icon.png` via `scripts/generate_android_assets.py`. Capacitor adaptive XMLs deleted | | P1 #5 Google Services plugin | ✅ Try-catch removed from `build.gradle`; explanatory comment in its place | | P1 #6 Branded splash | ✅ Icon on `#1a1a1a` at 5 densities × 2 orientations + the unqualified `drawable/splash.png` | | P1 #7 FileProvider | ✅ Stanza removed from manifest; `res/xml/file_paths.xml` deleted | ### New findings (round 2) **1. (75%) Android 12+'s `Theme.SplashScreen` API ignores `android:background`.** `android/app/src/main/res/values/styles.xml:19-21`: ```xml <style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen"> <item name="android:background">@drawable/splash</item> </style> ``` This is the Capacitor template's stock styling, but `Theme.SplashScreen` (the new Android-12+ splash API from androidx-core-splashscreen) doesn't honor `android:background` — it requires `windowSplashScreenBackground` (color) and `windowSplashScreenAnimatedIcon` (drawable, 432dp safe-zone). Practical impact: - **Android 6–11:** falls through to the legacy splash mechanism, shows our generated `splash.png` correctly. - **Android 12+:** uses the system splash with `@mipmap/ic_launcher` (our MetaScrub icon, which is good!) on a system-themed background (white in light mode, black in dark mode — NOT our `#1a1a1a`). On Android 12+ the user still sees the MetaScrub icon, just not on our chosen background. Acceptable for v1 sideload; track as a polish follow-up. Full fix needs a 432dp foreground icon with a 240dp safe zone (so adaptive masking doesn't crop it) — `scripts/generate_android_assets.py` could produce this in a future pass. **2. (60%) `dataExtractionRules` is API 31+ only — `tools:targetApi` annotation would silence lint warnings.** With `minSdk=23` and `android:dataExtractionRules="@xml/data_extraction_rules"`, AGP's lint will emit a `NewApi` warning (attribute unsupported on API < 31). Behavior is correct — the attribute is silently ignored on older API levels, and `allowBackup=false` + `fullBackupContent=false` cover those. Cleanest fix: ```xml <manifest xmlns:android="..." xmlns:tools="http://schemas.android.com/tools"> <application ... tools:targetApi="31"> ``` Doesn't change runtime behavior; just makes the lint output clean. Low priority. **3. (55%) Splash background `#1a1a1a` is arbitrary, not tied to the app's brand tokens.** `src/web/styles/tokens.css` has the canonical brand colors. The generator script hard-codes `#1a1a1a` near-black, which is close enough but isn't `var(--background-dark)` or whatever the actual token is. Worth aligning in a future pass. ### Things I verified are NOT broken - `styles.xml` references `@color/colorPrimary`, `colorPrimaryDark`, `colorAccent` — these are not defined in our `res/values/`, but resolve from `androidx.appcompat:appcompat:1.7.0`'s bundled resources. Capacitor 7 template intentionally ships no `colors.xml`; relies on appcompat. Verified no other code references the deleted `ic_launcher_background` color. - The deletion of `mipmap-anydpi-v26/` + `drawable-v24/` empty dirs is clean — manifest no longer references adaptive-icon resources. - `npx cap sync android` is unaffected — it copies `dist/web/` into `assets/public/`, doesn't touch icons or manifest. - `usesCleartextTraffic="false"` doesn't break Capacitor's `https://localhost/` (that scheme is intercepted by Capacitor's WebView, not Android's networking stack). - `data_extraction_rules.xml` is redundant with `allowBackup=false` (which already blocks everything) — but it's defense-in-depth and harmless. ### Carry-over P2 (unchanged from round 1) - versionName 4.0.0 vs CLAUDE.md/README "v5" — package.json housekeeping - No `NOTICE` / `THIRD_PARTY_LICENSES` for the new Apache-2.0 deps — F-Droid prep - Fallback path runner floor (Node 22 + apt) not explicitly documented in §CI ### Recommendation **Ready to merge.** All P0 and P1 from round 1 closed. The three new round-2 findings are P2 polish — split into follow-up issues after merge: 1. Android 12+ proper splash (windowSplashScreenBackground + animated foreground icon) 2. lint cleanup (`tools:targetApi` annotations) 3. Splash bg color from brand tokens The quality gates (`yarn typecheck`, `yarn lint`, `yarn test`, `yarn check:deps`) are all green. The actual Gradle build remains unverified locally (no JDK on this host) — the CI workflow is the verification path post-merge.
forgejo_admin merged commit e68a8141bf into master 2026-05-17 12:15:23 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: forgejo_admin/exifcleaner-web#156
No description provided.