Commit Graph

16 Commits

Author SHA1 Message Date
SoftwareApp ae05a0dad4 docs: add a hand-recording shot list for the FGS video
The script is a convenience, not a requirement: any Android 11+ phone can record the same
journey from the quick settings, with no adb and no tooling. Spell out the six steps so the
take can be done by hand if that is easier, with the two things a reviewer needs to be able to
follow - the trigger, and the service still working after the app leaves the screen.
2026-10-14 11:22:35 +02:00
SoftwareApp ea894841a2 docs: pin the FGS use case to Local processing > Other
The Console's Local processing options are Media transcoding, Importing/exporting and Other,
not the two-entry list the help page implies, so record which one this app takes and why the
other two are misdeclarations rather than near-misses:

- Media transcoding is converting media into a different format; the scan writes no media file
  at all, it only reads photos to hash and compare them.
- Importing/exporting is moving files in or out of the app; the app's only write is its own
  private hash index, which no other app ever sees - that is also what keeps "nothing leaves
  the device" true.
- Other is described by Play as "specifically user-initiated work and not for regular system or
  server-initiated tasks", which is exactly a scan the user taps Start scan for.

Worth spelling out because a use case that does not match the app is a misdeclaration, and the
description and the accompanying video have to support whichever option is chosen: "reads the
photo library and writes a small local index of file hashes" only fits Other.
2026-10-14 11:14:07 +02:00
SoftwareApp 1ce2f2e1f4 feat: record the foreground-service video Play's declaration asks for
The App content > Foreground service permissions form does not just want the justification that
was already written down. Per declared type it asks for four things, one of which is "a link to
a video demonstrating each foreground service feature. The video should demonstrate the steps
the user needs to take in your app in order to trigger the feature."

tools/record-fgs-demo.sh records exactly that journey on a device or emulator: the app on its
intro, the user tapping Start scan, Home, the ongoing notification visible in the shade while
the scan keeps running, and back into the app for the results. It loads the generated demo
library first so no real photo is in frame, writes a checkpoint PNG at each milestone so the
recording can be checked without decoding it, and takes the library off the device afterwards.
The output is git-ignored, like the screenshots.

Four things the script has to get right, each learned by getting it wrong first:

- bounds="[x1,y1][x2,y2]" cannot be parsed by stripping everything but digits: the "][ "
  between the corners goes too, which glued y1 to x2 and produced coordinates like
  "976 723061" - a tap that lands nowhere and reports no error at all.
- No single text matcher works: an exact match never finds the results card ("2 duplicate
  groups"), while a substring match lets "Scan" match both "Start scan" and "Scanned in
  39.7 s". Both matchers exist and each call site uses the right one.
- A finished scan leaves the app on its results screen, where the intro's button no longer
  exists - tapping those stale coordinates silently does nothing.
- `set -e` plus start="$(failing command)" aborts a run with no message whatsoever, which
  cost a whole debugging cycle.

Checklist step 5 now carries all four fields with paste-ready text: the description, the user
impact if the scan is deferred or interrupted (a scan is not deferrable work - the user is
waiting for a result, so there is no WorkManager/JobScheduler fallback), the use case
(Local processing: Other, the only dataSync option that describes a local, user-initiated
scan), and the video link.

Recording the take on this machine is not reliable, and the docs say so rather than pretending
otherwise, because both failures are platform-level rather than app-level:

- a cold app can miss Android's 5-second startForeground deadline and be killed, giving
  ForegroundServiceDidNotStartInTimeException -> Force finishing activity, i.e. the launcher
  where the scan should have started. The warm-up phase fixes it (a process that has started
  the service once never misses), but the fallback path of restarting the app gives the warmth
  back again, which is why the recorded scan is now triggered from the results screen's top-bar
  "Scan" instead of the intro's button.
- the app also died mid-scan on the emulator, with the launcher on screen afterwards and no
  exception of its own anywhere in logcat. No amount of warming fixes that one.

Neither reproduces on the real test device, where the same 22-photo library scans without
incident, so the take belongs on the phone. The script is the checklist for what it must show.
2026-10-14 11:13:21 +02:00
SoftwareApp f45e3b79ab docs: cover the tablet screenshot slots and settle app pricing
Two Console-facing subjects, both decided here instead of in the Console.

Tablet screenshots (7-inch and 10-inch)
--------------------------------------
Play keeps a separate screenshot slot per form factor and asks for at least four
screenshots on tablets, so the four phone captures did not cover them. Both sets are now
captured on AOSP Android 12 AVDs, over the same generated 22-photo demo library, and
normalised with FORM_FACTOR=tablet-7in|tablet-10in:

- 7-inch - Nexus 7 2013: 1200x1920 portrait, sw600dp, 320 dpi, four files
- 10-inch - Medium Tablet: 1600x2560 portrait, sw800dp, 320 dpi, four files

This machine has no /dev/kvm - the node cannot even be created, because mknod is refused
for the container as root - so the emulator ran in pure software emulation (-accel off
-no-window -gpu swiftshader_indirect): about 3 minutes to boot, 1-12 s per screenshot.
hide_error_dialogs was switched on and the animation scales off, because under TCG
SystemUI raises an ANR dialog that would otherwise sit in the frame; the 10-inch profile
boots landscape and its portrait rotation is 3, not 0. The scanning frame comes from a
screencap burst started before the tap, since 22 photos scan in seconds, and the tap
coordinates come from uiautomator dumps rather than guesswork.

The results screens are not staged: a finished scan auto-selects all but best, so "14
selected, 43.7 MB" is the default state. 2 duplicate groups, 16 of 22 photos, 43.7 MB
reclaimable, scanned in 39.7 s (7-inch) and 53.1 s (10-inch). Nothing except the demo
library was ever pushed to either AVD and it is deleted again afterwards, so no real
photo can appear in the tablet sets. Screenshots stay out of git, as before.

App pricing: a one-time paid app
--------------------------------
The one irreversible decision in this release. Play's rule: an app can go paid -> free,
but "once your app has been offered for free, the app can't be changed to paid" - a new
package name would be needed - so this had to be settled before the first upload, which
has not happened yet (versionCode 2).

Chosen over free with a Pro unlock because it is the only model that costs the app
nothing: no Billing dependency, no com.android.vending.BILLING permission, no Data
Safety answers, and the "no internet permission at all" claim survives untouched.
Checked rather than assumed: unpacking com.android.billingclient:billing:9.1.0 shows its
manifest declares only com.android.vending.BILLING and the billing service queries, so
in-app purchase would not have added INTERNET either. Billing 8+ is the current
requirement (latest 9.1.0) if that is ever revisited.

Price: base EUR 2.99 with a 14-day 50% launch sale at EUR 1.49. A EUR 1 base price was
considered and rejected: a sale can always be added, but a low anchor cannot be undone,
and a paid app's first transaction is its only transaction. Play's fee for a paid app's
initial purchase is 10% + 5% billing fee (~15%) for the first $1M/year of new-install
transactions in the EEA, so EUR 2.99 nets ~EUR 2.54 against ~EUR 0.85 at EUR 1. The sale
rules shape the launch: 1-14 days live, 30 days between one sale ending and the next
starting, and the app must be published before a sale can be created. The strikethrough
does appear in Germany, which is not one of the 12 EU states and the UK that hide it.

The listing copy had to stop saying "free": the short description is now "Find and remove
duplicate photos. Ad-free, works offline, no uploads." (69/80 chars) and the full
description gains one factual pay-once line plus a "sold through Google Play as a
one-time purchase" bullet (2,426/4,000). The price stays out of the title, icon and
developer name, because Play's metadata policy forbids price and promotional wording
there. PRIVACY.md gains a Purchases section - Google Play processes the payment, the app
has no payment code and no network access at all - and its date moves to 2026-10-14, so
the published copy needs re-pushing to Gitea.

No code changed, so the signed 1.0.1 .aab is untouched (sha256 f8e6a29a...). What is
left is Console work: a payments profile, the price and the launch sale on Products >
App pricing, and a promo-code copy to test the paid listing end to end.
2026-10-14 09:48:57 +02:00
SoftwareApp 5cb7ad66a8 feat: make the store-screenshot normaliser form-factor aware
Play gives phones and each tablet size a screenshot slot of their own (Phone, 7-inch
tablet, 10-inch tablet) and asks for at least four screenshots per large-screen form
factor, so the normaliser takes FORM_FACTOR=phone|tablet-7in|tablet-10in and keeps one
raw and one output directory per factor:

  store-assets/raw-screenshots/<form-factor>/   adb captures (git-ignored)
  store-assets/screenshots/<form-factor>/       what gets uploaded

The tablet form factors crop 0 px of system chrome on purpose, and that is checked
rather than guessed: every tablet profile in the SDK's own device definitions is already
inside Play's 2:1 cap (7in WSVGA 1024x600, 10.1in WXGA 1280x800, Nexus 7 2013
1200x1920, and Nexus 10 / Medium Tablet / Pixel Tablet at 2560x1600), so the 93/130 px
phone crop exists only because a 1080x2400 phone frame is 2.22:1. Cropping a tablet is
therefore cosmetic, and is now opt-in through STATUS_BAR_PX / NAV_BAR_PX.

The 2:1 clamp still runs for every form factor, and each file is now checked against
Play's limits as it is written (shorter side >= 320 px, longer side <= 3840 px) - the
phone captures only ever got that check by hand. A missing input directory or an unknown
form factor now fails with an actionable message instead of a bash error.

The four existing phone captures move to raw-screenshots/phone/ and normalise to the
same 1080x2160 RGB files as before, byte for byte. The 7-inch and 10-inch captures were
taken with this script on AOSP AVDs; how, and the numbers they show, are recorded in
docs/STORE_LISTING.md.

tools/make-store-assets.sh now prints the two tablet invocations as well, so the obvious
next step after generating the icon and feature graphic is not the phone-only one.
2026-10-14 09:48:49 +02:00
SoftwareApp 49025a823a docs: correct the Play tags to the ones that exist in Play taxonomy 2026-10-14 07:46:38 +02:00
SoftwareApp c9382364fc docs: pin down the Play tag set and the ones to avoid 2026-10-14 07:41:47 +02:00
SoftwareApp 89ddf3bb1d docs: point the privacy-policy step at the published Gitea remote 2026-10-14 07:24:49 +02:00
SoftwareApp f17b40c608 docs: tick the two checklist items that were already satisfied
The permission-set audit and the large-library scan test were both done, just not recorded:
aapt2 badging on the release APK matches the documentation table exactly (no INTERNET), and
the scans ran over 5,363 and 5,385 photos. Spell out what actually remains in the ROADMAP
paperwork row too - the keystore and versionCode are in place, so the open items are the
first upload + Play App Signing enrolment, the hosted privacy policy (still no git remote),
Data safety, the photo/video and foreground-service declarations, the App content questions
and the listing itself.
2026-10-14 00:13:06 +02:00
SoftwareApp 5ec2589444 docs: answer Play's native-debug-symbols warning
Uploading the bundle shows "This App Bundle contains native code, and you've not uploaded
debug symbols". Record the answer next to the upload step so it is not re-investigated on
every release:

- the bundle's only native code is libandroidx.graphics.path.so (4 ABIs, ~37 KB), which
  arrives transitively through androidx.compose.ui:ui-graphics ->
  androidx.graphics:graphics-path:1.0.1; this project builds no native code at all,
- ndk.debugSymbolLevel = "FULL" was tried and emits nothing for such a project (no
  native-debug-symbols output, nothing added to the bundle), and
- the published AAR ships stripped libs only (symtab=0, no .debug_* sections, no symbol
  entries at all), so there is no symbol file in existence to upload.

The warning is advisory and does not block review or rollout. Kotlin/Java stack traces are
already deobfuscated from the R8 mapping that rides inside the bundle, so the only thing
lost is symbolication of frames inside that Compose helper library.
2026-10-14 00:09:55 +02:00
SoftwareApp 55101886cb docs: finish the Console pre-flight and retire the screenshot blocker
Everything the Play forms ask for that can be settled from the working copy, settled:

- PRIVACY.md now carries the real contact address (softwareapp.hb@gmail.com), so the
  policy publishes as-is. Hosting is the one step left that cannot be done from here,
  because the repo has no git remote: step 5 of the checklist spells out the push +
  GitHub Pages route and the URL it yields.
- Checklist: the screenshot item is done (four captures, 1080x2160, over the demo
  library, no permission banner), the keystore and versionCode are recorded as already
  in place - including the upload key's SHA-256 for the Play App Signing enrolment - and
  the App access / Ads / remaining App content declarations are written down. The
  first-upload release-notes question is answered too: versionCode is already 2, so
  1.0.1 takes the *initial release* block, not the 1.0.1 one.
- The build facts were verified against the artefacts rather than the source: the
  release APK reports versionCode=2, versionName=1.0.1, targetSdk=36 and exactly the
  documented permission set, with no INTERNET in what actually ships.
- ROADMAP: the re-shoot row is struck out with the numbers that closed it, M10 no longer
  claims the captures predate the demo library, and the demo library's removal is
  recorded (MediaStore 5,386 -> 5,364; pruneTo drops the stale index rows next scan).

The staged state and the system confirmation dialog are documented as deliberately not
captured: staging clears the demo group, which would put real photos back in frame.
2026-10-13 23:59:15 +02:00
SoftwareApp b43718aca8 docs: record the store screenshot re-shoot over the demo library
The three captures no longer show the "Photo access needed" banner, and the results
screen is a picture of the generated demo library instead of the developer's own.
Both caveats in the file are closed, so replace them with a short note on how the set
was actually taken:

- 01-empty: the grant went through the app's own "Grant access" button, so
  checkSelfPermission is genuinely satisfied. This one needed no tap at all: results
  and the selection live in ScannerViewModel and only the hash index is persisted, so
  force-stop + relaunch returns to the empty state with the grant still in place.
- 02-scanning / 03-results: shot after tapping Scan over the demo library. The headline
  group is 43.1 MB reclaimable, which out-ranks the biggest group already on the device
  (39.8 MB), so the first group in frame is generated.
- 04-options is new: the Options DropdownMenu with the three sensitivity presets (it is
  not a bottom sheet) and the trash-vs-permanent toggle.

The tick marks and the "37 selected" bar are the screen's default state, not a staged
tap -- a finished scan auto-selects all but best. MIUI still refuses pm grant and
synthetic taps (INJECT_EVENTS), so Grant access and Scan remain the two taps a human
has to make; the two screens still missing (the staged state and the system consent
dialog) are captured by watching for UI text changes instead, rather than by timing a
scripted tap.
2026-10-13 23:46:50 +02:00
SoftwareApp ca8fdd414d docs: stop describing the store screenshots as committed
store-assets/screenshots/ is git-ignored now, so the Graphics section and the
two blocker entries were describing a state that no longer exists. Point at
where the captures live instead, and say why screenshots stay out of git at
all: a screenshot of this app is a picture of a photo library.
2026-10-13 23:35:45 +02:00
SoftwareApp 3581ffc6bb Add a generated demo photo library for store screenshots
The results screenshot is a screenshot *of a photo library*, so the committed
capture showed the developer's own photos. tools/make-demo-photos.py draws the
library instead: 22 procedural scenes (sunset, mountains, beach, conifers in
haze, dunes, city skyline, night sky, bokeh) with grain and a vignette, so there
are no people, no third-party content and nothing to license.

The duplicate structure is deliberate, because a folder of unrelated pictures
demonstrates nothing:

- headline group: one scene re-saved at falling quality and size until its
  reclaimable size reaches --target-mb (45 MB). Results are ranked by
  reclaimable bytes, so this group has to out-rank whatever is already on the
  device or a real photo is the first thing back in frame.
- exact pair: two byte-identical files, for the SHA-256 path.
- singles: the other scenes, once each, so the library looks lived-in.

Verified with the app's own engine on the desktop before pushing anything:
22 images -> 2 groups (PERCEPTUAL 14 copies, EXACT 2 copies) with the six
singles left alone. Regeneration is byte-identical, the output path resolves
from __file__ so it runs from any cwd, and nothing is written outside the repo.

demo-photos/ is git-ignored, like fixtures/ and store-assets/raw-screenshots/.
STORE_LISTING.md documents the library, why the headline group has a target
size, the MediaProvider scan_file recipe and the cleanup step; ROADMAP, README
and the release checklist point at it.
2026-10-13 23:34:12 +02:00
SoftwareApp 1145944659 chore: keep store screenshots out of git 2026-10-13 23:17:48 +02:00
Hadrian Burkhardt 4706e2669c Initial commit: Foto Dup Finder 1.0.1
An Android duplicate-photo finder. Pure-Kotlin core (SHA-256 + dHash hashing, exact and
perceptual union-find grouping, three sensitivity presets) with MediaStore access, a
persistent SQLite hash index and a tiered ScanEngine behind a Jetpack Compose UI.
No INTERNET permission, no ads, no analytics, no account.

Ships versionCode 2 / versionName 1.0.1, carrying the scan-stall hardening (BoundedCall
breaker) and verified against a 5,363-photo device library. 80 JVM unit tests, lint clean.

Release paperwork is in docs/ (Play checklist, store listing, edge-case testing), the
upload-ready Play graphics and screenshots in store-assets/, and the generators for those
assets in tools/.
2026-10-13 23:13:39 +02:00