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.
This commit is contained in:
@@ -75,15 +75,43 @@ Ready-to-paste copy and the generated graphics live in
|
||||
[docs/STORE_LISTING.md](STORE_LISTING.md) (milestone M10). Everything below is a Console
|
||||
action except the items marked **[code]**.
|
||||
|
||||
- [ ] App name (≤ 30 chars): "Foto Dup Finder".
|
||||
- [ ] ⚠️ **App pricing — do this before the first upload; it is the one irreversible decision.**
|
||||
Play, *Set up your app's prices*: *"You can change your app from paid to free. Once your app has
|
||||
been offered for free, the app can't be changed to paid. If you want to charge for the app, you
|
||||
need to create a new app with a new package name and set a price."* This release has not been
|
||||
uploaded yet (`versionCode` 2 is the first), so a paid app is still possible; after the first
|
||||
upload to any track it is not. In the Console:
|
||||
1. **Set up a payments profile** with your tax rates (Play asks the first time you set a price).
|
||||
2. **Products > App pricing** → *"Your app is"* → **Make your app paid**, and enter the base price
|
||||
**€2.99** (one-time). Leave *"Default price includes tax"* off unless €2.99 is meant to be the
|
||||
final gross price in the EU — off, a German buyer pays €3.56.
|
||||
3. Check the **local prices** Play derives for each country — a country with no price is a
|
||||
country where the app is unavailable.
|
||||
4. [ ] **Launch sale: 50% off (€1.49) for 14 days.** A sale can only be created *after* the app is
|
||||
published (*App pricing > Sales > Create sale*), it can be **live for 1–14 days at most**, and
|
||||
**30 days must pass between one sale ending and the next starting** — so this is a single
|
||||
deadline-driven window, not a permanently lower launch price. Percentage discounts must be
|
||||
between 10% and 100%. The original price is struck through everywhere except in 12 EU member
|
||||
states and the UK (Germany does show it).
|
||||
5. [ ] **Test the paid listing end to end**: install your own copy through a **promo code**
|
||||
rather than a real purchase. A giveaway code is the only trial mechanism a paid app has, and
|
||||
it doubles as the check that a buyer really does receive a working app.
|
||||
**[code]** nothing to build: no Billing dependency, no `com.android.vending.BILLING` in the
|
||||
merged manifest. Rationale, the fee arithmetic and the sale rules:
|
||||
[STORE_LISTING](STORE_LISTING.md) › App pricing.
|
||||
- [ ] App name (≤ 30 chars): "Foto Dup Finder" (15 chars).
|
||||
- [ ] Short description (≤ 80 chars) — two options are drafted and length-checked in
|
||||
STORE_LISTING.md.
|
||||
- [ ] Full description (≤ 4000 chars) — drafted and length-checked in STORE_LISTING.md.
|
||||
STORE_LISTING.md (69 and 65 chars). **Neither says "free"**: the app is paid, so the claim would
|
||||
be false, and Play's metadata policy keeps price and promotional wording out of the title,
|
||||
icon and developer name.
|
||||
- [ ] Full description (≤ 4000 chars) — drafted and length-checked in STORE_LISTING.md (2 426 chars,
|
||||
so there is room to grow). The one price line ("Pay once, then own it…") sits in the body for
|
||||
the same policy reason.
|
||||
- [x] **App icon** 512×512 PNG **[code]** — `tools/make-store-assets.sh` renders it from the
|
||||
same vectors as the adaptive icon in `res/drawable/ic_launcher_*`.
|
||||
- [x] **Feature graphic** 1024×500 PNG **[code]** — same script.
|
||||
- [x] **Screenshots** (at least 2, up to 8) **[code]** — four are captured and normalised to
|
||||
1080×2160 in `store-assets/screenshots/` (git-ignored): empty state, scanning
|
||||
- [x] **Phone screenshots** (at least 2, up to 8) **[code]** — four are captured and normalised to
|
||||
1080×2160 in `store-assets/screenshots/phone/` (git-ignored): empty state, scanning
|
||||
(`5353 / 5385`), results (**17 duplicate groups, 54 of 5385 photos, 83.5 MB reclaimable**)
|
||||
and the Options menu. They were shot after a real permission grant and over the generated
|
||||
demo library (`tools/make-demo-photos.py`), so neither the "Photo access needed" banner nor
|
||||
@@ -94,6 +122,24 @@ action except the items marked **[code]**.
|
||||
⚠️ Play rejects a screenshot whose longer side is more than 2× its shorter side, so a
|
||||
1080×2400 phone capture has to be cropped to 1080×2160;
|
||||
`tools/make-store-screenshots.sh` does exactly that (status and navigation bars only).
|
||||
- [x] **7-inch tablet screenshots** **[code]** — captured on a `Nexus 7 2013` AOSP Android 12 AVD
|
||||
(1200×1920 portrait, `sw600dp`) and normalised to `store-assets/screenshots/tablet-7in/`
|
||||
(git-ignored) with `FORM_FACTOR=tablet-7in ./tools/make-store-screenshots.sh`: the same four
|
||||
screens as the phone set, over the same generated demo library — empty state, scanning
|
||||
(*“Looking for visually similar photos…”* 20 / 22), results (**2 duplicate groups, 16 of 22
|
||||
photos, 43.7 MB reclaimable**, scanned in 39.7 s) and the Options menu (✓ on *Balanced*).
|
||||
Play asks for **at least 4 screenshots** on tablets, so four is the floor, not the target.
|
||||
📋 A tablet frame is already inside Play's 2:1 cap, so this form factor deliberately crops
|
||||
**0 px** of system chrome; the tablet captures are the raw screencaps, flattened to 24-bit.
|
||||
- [x] **10-inch tablet screenshots** **[code]** — same run on a `medium_tablet` AOSP Android 12 AVD
|
||||
(1600×2560 portrait, `sw800dp`), normalised into `store-assets/screenshots/tablet-10in/` with
|
||||
`FORM_FACTOR=tablet-10in`: scanning at 20 / 22 and the same **2 groups · 16 of 22 photos ·
|
||||
43.7 MB reclaimable** results screen, scanned in 53.1 s.
|
||||
⚠️ Both AVDs ran in **pure software emulation**, because this machine exposes no `/dev/kvm`
|
||||
(`-accel off -no-window -gpu swiftshader_indirect`): ≈3 min to boot and 1–12 s per screenshot.
|
||||
`hide_error_dialogs` was turned on and animations off, because under software emulation SystemUI
|
||||
raises an ANR dialog that would otherwise sit in frame. Full recipe:
|
||||
[STORE_LISTING](STORE_LISTING.md) › Screenshots › 7-inch and 10-inch tablets.
|
||||
- [ ] Category: *Tools* (or *Photography* — pick it **before** the tags, because it shapes the
|
||||
*Suggested tags* list). Tags: **max 5, from Play's own list, not typed freely — and only two
|
||||
are honest for this app: *Photography*, plus optionally *Privacy & security*.** Play's
|
||||
@@ -129,6 +175,13 @@ action except the items marked **[code]**.
|
||||
- *Is all user data encrypted in transit?* → N/A (no transmission).
|
||||
- *Do you provide a way for users to request data deletion?* → Uninstalling
|
||||
removes all app data.
|
||||
- The **paid** model changes none of the above: the purchase happens in the Play Store app, not in
|
||||
this one. Play's own guidance is that using a payment service such as *"Google Play's billing
|
||||
system"* to complete transactions does **not** require declaring the data that service collects
|
||||
in connection with its processing — and this app contains no payment code at all, so it receives
|
||||
nothing either way. Re-read that if a Billing dependency is ever added
|
||||
(step 9); the app's own answers would still be "no", but the *purchase handling* would move
|
||||
into the app and the answers would need a fresh look.
|
||||
- [x] Confirm the **permission set** the Console will show. **[code]** done 2026-10-13: `aapt2 dump
|
||||
badging` on the release APK reports exactly the table below (plus
|
||||
`…DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION`) — and **no `INTERNET`**. Re-check from the
|
||||
@@ -282,13 +335,25 @@ Ready-to-paste block for **1.0.1** (the scan-robustness patch):
|
||||
- [ ] Optionally add crash reporting later — but note this would change the Data
|
||||
safety answers, so weigh it against the "no data leaves the device" promise.
|
||||
|
||||
## 9. Optional monetisation
|
||||
## 9. Monetisation — decided: one-time paid app, no billing
|
||||
|
||||
The app currently has no ads and no billing, which is a deliberate differentiator.
|
||||
If you later add a paid tier, use **Play Billing** (e.g. a one-time "Pro"
|
||||
purchase) and update:
|
||||
- the Data safety form (purchase history),
|
||||
- the privacy policy,
|
||||
- the store description.
|
||||
**The decision is made: Foto Dup Finder is a one-time paid app** (rationale, the €2.99 base price,
|
||||
the €1.49 launch sale and the irreversibility rule: [STORE_LISTING](STORE_LISTING.md) › App pricing).
|
||||
The Console work is in step 4; **nothing in this repository changes**, because a price on the listing
|
||||
adds no code, no permission and no Data Safety answer — which is exactly what a free app with a Pro
|
||||
unlock could not claim.
|
||||
|
||||
Avoid ad SDKs entirely — they would contradict the app's main selling point.
|
||||
Standing rules:
|
||||
|
||||
- **Ads: never.** Every ad SDK needs the `INTERNET` permission to fetch anything, and the
|
||||
merged-manifest check in step 5, [PRIVACY.md](../PRIVACY.md) and the Data Safety answers all rest
|
||||
on that permission being absent.
|
||||
- **In-app purchases: deliberately absent.** If that ever changes, the requirement is Play Billing
|
||||
**8+** (latest published: 9.1.0, with an extension available to Nov 1, 2026). Worth knowing before
|
||||
the decision is re-litigated: the **Billing Library itself does not add `INTERNET`** —
|
||||
`com.android.billingclient:billing:9.1.0`'s manifest declares only `com.android.vending.BILLING`
|
||||
and `<queries>` for the billing service, which was checked by unpacking the AAR. What *would*
|
||||
change is the store copy (the "pay once" line), `PRIVACY.md`, the acknowledgement/restore flow, and
|
||||
the fact that a purchase can only be verified on an internal testing track — never locally.
|
||||
- **Refunds and support** follow Play's policy for a paid app; keep the contact address in
|
||||
[PRIVACY.md](../PRIVACY.md) current.
|
||||
|
||||
Reference in New Issue
Block a user