feat(android): Capacitor APK wrapper + on-demand CI build #156
No reviewers
Labels
No labels
bug
documentation
duplicate
e-copy
e-features
e-mobile
enhancement
f-coverage
f-forensic
f-perf
f-privacy
forensic
good first issue
help wanted
infra
invalid
phase-a
phase-b
phase-c
phase-d
phase-e
phase-f
phase-g
phase-h
priority-1
priority-2
priority-3
privacy
question
v5
v6
video-hardening
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: forgejo_admin/exifcleaner-web#156
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/android-apk-issue-153"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #153.
What
dist/web/bundle..github/workflows/build-android.yml— manually-triggered (workflow_dispatchonly) CI workflow that builds the APK and uploads it as a workflow artifact.PRIVACY_GAPS.mdupdated with the Android ≤ 9Downloads/world-readable gap.docs/superpowers/plans/2026-05-17-android-apk-and-ci.md.Decisions (Q1–Q5 from issue #153)
Downloads/for v1; documented as a privacy gap on Android ≤ 9 (PRIVACY_GAPS.md).android/placementmaster.workflow_dispatchonly — per founder direction: "started manually not on merge or PR."How to test the CI workflow
master.metascrub-debug-apkartifact (~5–10 MB).Downloads/.Local commands (require JDK 17 + Android SDK installed)
Privacy invariants
connect-src 'self'fromvite.config.web.ts).npx cap initran with telemetry declined.server.androidScheme: 'https'set explicitly so WASM and service workers stay in a secure context.*.jks/*.keystore/app/release/added toandroid/.gitignore(was commented out in the Capacitor template).Quality gates
yarn typecheck✓yarn lint✓yarn test✓ (509/509)yarn check:deps✓ (no circular deps)Out of scope (deferred)
@capacitor/filesystemadapter for the Android ≤ 9Downloads/mitigation (documented as a follow-up inPRIVACY_GAPS.md).npx cap add ios).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%)
INTERNETpermission needs to be documented as a privacy invariant gap.android/app/src/main/AndroidManifest.xml:40declares<uses-permission android:name="android.permission.INTERNET" />. Capacitor needs this for thehttps://localhost/WebView scheme — even local-served bundled assets require the platform INTERNET grant. The CSPconnect-src 'self'blocks remote calls at the WebView layer, but anyone auditing the APK withaapt dump permissionswill 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.mdexplaining (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:5defaults to allowingadb backupand 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. Setandroid:allowBackup="false"in<application>— one-line fix.3. (75%) Workflow
container:expression may not evaluate correctly..github/workflows/build-android.yml:38uses:GitHub Actions converts
workflow_dispatchboolean 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: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.pngare the Capacitor Android template defaults (verified:diffagainst.resources/icon.pngreturns 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(orstatic/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: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.pngare 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:path="."exposes every file under each base path via FileProvider URIs. Mitigated byandroid: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.jsonis4.0.0, CLAUDE.md / README reference v5.I matched
versionName "4.0.0"inandroid/app/build.gradletopackage.json. If the v5 rebrand is real,package.jsonneeds a bump first, then Android version follows. Separate housekeeping issue.9. (55%) No
NOTICE/THIRD_PARTY_LICENSESfor 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=falseassumes the runner provides Node 22 + yarn + apt. Forgejo-stack default, GitHub-hosted, and standardact_runnerconfigs all qualify. A barenode:slimdoes not. Worth one line indocs/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.javais the Capacitor defaultBridgeActivity— no custom code paths to audit.capacitor.config.tscorrectly setsserver.androidScheme: 'https'.vite.config.web.tsis unchanged..gitignorecorrectly excludes keystores (*.jks,*.keystore,app/release/).package.jsonprod deps stayed at 4 (Capacitor went to devDependencies).yarn check:deps(madge --circular) passes.yarn testpasses (509/509).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 round 2 — PR #156
Verifying round 1's fixes + scanning for newly introduced issues.
Round 1 P0/P1 verification — all closed
docs/PRIVACY_GAPS.mdhas the new "Android INTERNET permission" section + inline comment inAndroidManifest.xmlallowBackup=false+fullBackupContent=false+dataExtractionRulesdeny-all for Android 12+. Also addedusesCleartextTraffic=false== true/!= trueapplied at the three sites inbuild-android.yml.resources/icon.pngviascripts/generate_android_assets.py. Capacitor adaptive XMLs deletedbuild.gradle; explanatory comment in its place#1a1a1aat 5 densities × 2 orientations + the unqualifieddrawable/splash.pngres/xml/file_paths.xmldeletedNew findings (round 2)
1. (75%) Android 12+'s
Theme.SplashScreenAPI ignoresandroid:background.android/app/src/main/res/values/styles.xml:19-21:This is the Capacitor template's stock styling, but
Theme.SplashScreen(the new Android-12+ splash API from androidx-core-splashscreen) doesn't honorandroid:background— it requireswindowSplashScreenBackground(color) andwindowSplashScreenAnimatedIcon(drawable, 432dp safe-zone). Practical impact:splash.pngcorrectly.@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.pycould produce this in a future pass.2. (60%)
dataExtractionRulesis API 31+ only —tools:targetApiannotation would silence lint warnings.With
minSdk=23andandroid:dataExtractionRules="@xml/data_extraction_rules", AGP's lint will emit aNewApiwarning (attribute unsupported on API < 31). Behavior is correct — the attribute is silently ignored on older API levels, andallowBackup=false+fullBackupContent=falsecover those. Cleanest fix:Doesn't change runtime behavior; just makes the lint output clean. Low priority.
3. (55%) Splash background
#1a1a1ais arbitrary, not tied to the app's brand tokens.src/web/styles/tokens.csshas the canonical brand colors. The generator script hard-codes#1a1a1anear-black, which is close enough but isn'tvar(--background-dark)or whatever the actual token is. Worth aligning in a future pass.Things I verified are NOT broken
styles.xmlreferences@color/colorPrimary,colorPrimaryDark,colorAccent— these are not defined in ourres/values/, but resolve fromandroidx.appcompat:appcompat:1.7.0's bundled resources. Capacitor 7 template intentionally ships nocolors.xml; relies on appcompat. Verified no other code references the deletedic_launcher_backgroundcolor.mipmap-anydpi-v26/+drawable-v24/empty dirs is clean — manifest no longer references adaptive-icon resources.npx cap sync androidis unaffected — it copiesdist/web/intoassets/public/, doesn't touch icons or manifest.usesCleartextTraffic="false"doesn't break Capacitor'shttps://localhost/(that scheme is intercepted by Capacitor's WebView, not Android's networking stack).data_extraction_rules.xmlis redundant withallowBackup=false(which already blocks everything) — but it's defense-in-depth and harmless.Carry-over P2 (unchanged from round 1)
NOTICE/THIRD_PARTY_LICENSESfor the new Apache-2.0 deps — F-Droid prepRecommendation
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:
tools:targetApiannotations)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.