1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# Releasing
## One-time setup
### 1. Generate an upload key
Google holds the real app signing key under Play App Signing and re-signs every
release. What you generate here is only the *upload* key — if it's lost, Play
support can reset it, which is not true of the app signing key.
```bash
keytool -genkeypair -v \
-keystore upload-keystore.jks \
-alias upload \
-keyalg RSA -keysize 2048 -validity 10000
```
Then create `keystore.properties` in the repo root:
```properties
storeFile=upload-keystore.jks
storePassword=<store password>
keyAlias=upload
keyPassword=<key password>
```
Both files are gitignored. Keep a backup somewhere other than this machine —
without them you can still recover, but only by going through Play support.
Release builds without `keystore.properties` are left unsigned rather than
failing, so a fresh clone and CI can still compile the release variant.
### 2. Play Console
- Create the app. The package name **`social.grain` is permanent** once the
first build is uploaded and cannot be changed afterwards.
- Under **Setup → App signing**, opt into Play App Signing (the default for new
apps) and let Google generate the app signing key.
- Fill in the **Data safety** form. The app collects the account handle and DID,
the photos a user chooses to publish, and their EXIF when the poster leaves
camera data enabled. All of it goes to the user's own PDS via grain.social.
It requests `INTERNET` and, for the story composer, `CAMERA`; photos taken
there are only ever sent when the user posts them.
- Content rating questionnaire, target audience, and a privacy policy URL are
all required before any track can go live.
Note for personal developer accounts created after 13 Nov 2023: production is
locked until a closed test has run with at least 12 testers opted in for 14
continuous days. Check **Testing → Closed testing** — a requirement card there
means the rule applies.
## Cutting a release
Bump `versionCode` and `versionName` in `app/build.gradle.kts`. `versionCode`
must increase with every upload, even for a build that never ships.
```bash
./gradlew bundleRelease
```
The bundle lands at `app/build/outputs/bundle/release/app-release.aab`. Upload
that, not the APK — Play requires the App Bundle format.
To sanity-check a release build on a device first, the APK variant can be signed
with the debug key and installed locally:
```bash
./gradlew assembleRelease
zipalign -f 4 app/build/outputs/apk/release/app-release-unsigned.apk /tmp/aligned.apk
apksigner sign --ks ~/.android/debug.keystore --ks-pass pass:android \
--ks-key-alias androiddebugkey --key-pass pass:android \
--out /tmp/release-test.apk /tmp/aligned.apk
adb install -r /tmp/release-test.apk
```
This is worth doing at least once per dependency bump. R8 strips
kotlinx.serialization's generated `$$serializer` classes without the keep rules
in `proguard-rules.pro`, and the failure is release-only: every network decode
throws and the feed simply comes back empty.
## Store assets
- Icon: `store/play-store-icon-512.png` (regenerate with
`python3 tools/generate_icons.py`).
- Feature graphic: `store/play-feature-graphic-1024x500.png` (regenerate with
`python3 tools/generate_feature_graphic.py`).
- Screenshots: `store/play-screenshot-*.png`. Capture with `adb exec-out
screencap -p > shot.png`, then fit them to Play's rules with `python3
tools/prepare_screenshots.py shot.png --out store/` — a raw capture is 2.06:1,
which is past the 2:1 Play accepts, and the script pads each to 1080x1920
rather than cropping the app's own chrome away.
Sign in on an emulator rather than using a phone: a locked screen returns a
black `screencap` and blocks instrumented tests. Use your own galleries — the
feed shows other people's photographs, which aren't yours to put in a store
listing.