6.7 KiB · text History 017d162

FeedPug Android client

A minimal Kotlin / Jetpack Compose reader for FeedPug. It authenticates with a per-user API token using the same device-pairing flow as ../dms:

  1. In the FeedPug web app, open the account menu → Devices, generate a token.
  2. The page shows a QR encoding feedpug://pair?base=<server>&token=<token>.
  3. The app scans the QR (or you open the URI / paste the fields), verifies it by calling GET /api/profile, and stores (baseUrl, token) in EncryptedSharedPreferences.
  4. Every request then sends Authorization: Bearer <token>.

Features (MVP)

  • Pair screen: QR scan (ZXing) + manual URL/token entry + verify.
  • Timeline: aggregated newsfeed, full-text search over entry bodies, your saved views as filter chips (tap to apply, “All” to clear), unread-only toggle, “load more” keyset pagination, mark-all-read, unpair. Swipe a row left to mark that entry unread.
  • Detail: full entry rendered in a (JavaScript-disabled) WebView, marks the entry read on open, toggle reactions from your palette, open original in browser.
  • Theming: FeedPug’s Blueprint default plus the shared neiam OKLCh palettes (Her, After Dark, Forest, Sky, Clays, Stones, Dark, Light) and the B612 typeface — the same set as the web frontend. Pick a theme from the palette menu in the timeline top bar; the choice persists in its own encrypted prefs file.

Build

Requires Android Studio (Koala+) or a local Android SDK with JDK 17–21. Point at your SDK via local.properties (sdk.dir=/path/to/Android/Sdk) or ANDROID_HOME.

cd android
./gradlew :app:assembleDebug      # builds app/build/outputs/apk/debug/app-debug.apk
./gradlew installDebug            # install on a connected device/emulator

Builds clean against AGP 8.5.2 / Gradle 8.9 (the wrapper is committed). Verified to produce a debug APK with SDK platform 34 + build-tools 34/35/36.

Connecting to a dev server

  • The manifest sets usesCleartextTraffic="true" so you can point at a plain http:// LAN address during development.
  • On the Android emulator, the host machine is reachable at http://10.0.2.2:4000. Generate the web token while serving on that host, or just edit the Server URL field on the Pair screen after scanning.
  • For real devices, use your machine’s LAN IP (e.g. http://192.168.1.50:4000) or set FEEDPUG_PUBLIC_URL on the server so the QR encodes a reachable origin.

Layout

app/src/main/java/org/neiam/feedpug/app/
  MainActivity.kt            # nav host + feedpug://pair deep-link handling
  data/PairUri.kt            # parse feedpug://pair?base=&token=
  data/TokenStore.kt         # EncryptedSharedPreferences credential store
  data/ThemeStore.kt         # persisted theme preference
  data/Models.kt             # kotlinx.serialization API models
  data/FeedPugClient.kt      # Retrofit API + Bearer interceptor
  ui/PairScreen.kt           # QR scan + manual pairing
  ui/TimelineScreen.kt       # newsfeed list + theme menu
  ui/DetailScreen.kt         # entry reader + reactions
  ui/theme/AppTheme.kt       # neiam OKLCh palettes (ALL_THEMES)
  ui/theme/Typography.kt     # B612 typography
  ui/theme/Theme.kt          # FeedPugTheme wrapper (palette + type)
res/font/                    # b612_regular.ttf, b612_bold.ttf

Release builds

CI builds and signs the release APK in a container (android/Dockerfile) and publishes it to the forge’s own F-Droid repository — nothing is installed on the runner, and the same command reproduces it locally:

docker buildx build --target android-export \
  --secret id=keystore,src=./release.jks \
  --secret id=keystore_password,src=./keystore.pass \
  --output type=local,dest=./apk --file android/Dockerfile .

The keystore is a BuildKit secret, so it never reaches a Gradle script, a Gradle cache, or an image layer. Signing is a separate step from Gradle for the same reason.

.forgejo/workflows/android.yml runs that on every push to master and POSTs the APK to /pkg/fdroid/<owner>/<repo>/upload with the job’s own token — there is no publishing credential to create.

versionCode comes from the CI run number and versionName from the commit, so every published build sorts after the last one. Local builds stay at versionCode = 1 / …+dev; they are never published.

Install from the repository by adding it in F-Droid:

<forge>/pkg/fdroid/<owner>/_all?fingerprint=<fingerprint>
curl -fsSL <forge>/pkg/_keys/fdroid/<owner>    # prints the fingerprint

One entry covers every Android app the owner publishes, this one included.

Two different keys, deliberately. The APK key below is what Android checks on every update. The index key is per-owner, lives in the forge’s OpenBao transit engine, and never comes near CI.

The release keystore

Generate it once. Android verifies this certificate on every update, so losing it means every install has to be uninstalled and redone by hand — there is no recovery and no rotation.

keytool -genkeypair -v \
  -keystore release.jks \
  -storetype PKCS12 \
  -alias feedpug \
  -keyalg RSA -keysize 4096 \
  -validity 10000 \
  -dname "CN=FeedPug, O=neiam, C=US"

It prompts for the password rather than taking -storepass, which keeps it out of shell history. PKCS12 uses one password for the store and the key, which is what android/Dockerfile assumes — it passes the one password file to --ks-pass and lets the key password default to it.

Back it up before it exists anywhere else

So there is never a window where your laptop holds the only copy:

export BAO_TOKEN=<root token>
E() { kubectl -n openbao exec -i statefulset/openbao -- env BAO_TOKEN="$BAO_TOKEN" "$@"; }

E bao kv put kv/feedpug/android \
    keystore="$(base64 -w0 release.jks)" \
    password='<the password>'

kv-v2 versions writes, so a later overwrite doesn’t destroy this one. Recover with:

E bao kv get -field=keystore kv/feedpug/android | base64 -d > release.jks

Hand it to CI

On the repository: Settings → Actions → Secrets.

Secret Value
ANDROID_KEYSTORE base64 -w0 release.jks
ANDROID_KEYSTORE_PASS the password

The android job skips cleanly while these are unset, so nothing breaks before you get to it. It refuses to fall back to a debug-signed APK: that key is shared by everyone with an SDK, so publishing one would let anyone ship an “update” to it.

CI does not read the keystore out of OpenBao. apksigner has no external-signer hook, so the key has to reach the build container whichever store it comes from, and the runner is outside the cluster while OpenBao has no ingress on purpose. Bao holds the durable copy; the Forgejo secret is the operational one.