MaksForge

Darek

Android app

A Polish voice assistant that opens your gate with a phone call, offline.

Building

Definition of done — what is still open

The live checklist. Keep it short: when an item closes, tick it, write the result into whatever long-form record you keep, and delete the detail from here. A session should be able to pick up the work by reading this file alone.

The standing schema gate — parked

Parked 2026-10-31: Darek has no database (ADR-0001’s amendment). It comes back, as written below, the day a feature adds a table.

An update must migrate an existing install without losing anything (ADR-0001). This item does not close. Whenever APP_SCHEMA_VERSION changes, all five hold before the release goes out:

  1. MIGRATION_x_y written and registered in APP_MIGRATIONS for every step. A migration Room never runs is not a migration, and nothing about the build complains.
  2. The exported app/schemas/*/N.json committed — every later migration is transcribed from it, never from the entity classes.
  3. SchemaGateTest extended, so the launch gate is proven to let the upgrade through. Every migration test opens the database directly and walks past that gate; this assertion is the only thing standing in front of it.
  4. A migration test proving the rows survive — read values back, do not merely assert nothing threw. runMigrationsAndValidate compares shapes and is blind to a table emptied by a cascading DROP.
  5. An actual upgrade watched on a phone: install the previous build, put real data in it, install the new one over the top, confirm the app opens and the data is there. ./gradlew assembleDebug -PreleaseShapedDebug is how to do this without touching a Play install — it is minified and BuildConfig.DEBUG == false, so migrations are registered rather than the destructive fallback. ⚠️ Both halves are load-bearing: a plain debug build takes the fallback and proves the opposite of what you wanted.

scripts/schema-gate.py enforces 1–3 mechanically in CI. Items 4 and 5 are yours.

Before the first upload

  • Delete what the app does not need, while deleting is free — optional-modules.md lists each module’s files and the five places around the code a strip reaches. Room, photos, backup and reminders are each a data-loss or Data safety story you will otherwise defend forever. Done 2026-10-31: all four deleted, and the placeholder Items with them; the tip stays.
  • Decide minSdk deliberately, before launch. The direction is asymmetric: raising it later strands existing installs on the last build that fitted them, lowering it later is free. If the only phone in the loop sits at the top of the range, every SDK_INT branch below it ships unvalidated — the first app built from this template raised 26 to 33 for exactly that reason. Move CI’s floor legs with it, and write the reason beside minSdk in app/build.gradle.kts, where the next person to touch it reads it.
  • Replace the placeholder mark. art/mark.py, then python3 art/make-launcher-icon.py and make-feature-graphic.py. Commit the concept image and record its provenance in art/README.md — where the art came from is the thing most likely to block a first upload, and it is discovered late.
  • Replace the placeholder domain. data/ItemEntity.kt and ui/items/ — or delete them (above). Done 2026-10-31: deleted, with Darek’s Home screen in their place.
  • Rewrite scripts/edge-to-edge.py’s SCENES against your screens. Until then the nightly walks screens that do not exist, and screenshots.py cannot shoot a listing. Done 2026-10-31: Home, Settings, Help and the Licences.
  • Make the artifact allowlists yours. aab-permissions.py’s EXPECTED, FORBIDDEN and EXPECTED_ORIENTATION describe the template. Run all four aab-*.py scripts against a local bundleRelease before the first tag — the first app built from it found its stale table in the publish workflow, after the tag, with the version already spent.
  • Choose the palette. Four seeds in scripts/gen_scheme.py, regenerate theme/Color.kt, read the contrast report it prints on stderr. Done 2026-10-31: cobalt and amber, phase 0’s checkpoint C.
  • Make the support address work both ways. bootstrap.py compiled in <repo>.support@maksforge.com (or --support) and wrote it into the site, the privacy policy and the Play docs. Mail to it arrives through the domain’s catch-all; replying from it needs the receiving Gmail’s Send mail as entry, confirmed. Send Report a problem from a non-Gmail account and check it lands outside Spam. One address everywhere — the listing’s contact field and IARC’s rating contact too; ui/support/SupportConfig.kt says why it lives on our own domain.
  • Write the listing. docs/store-listing.md — every heading in it is parsed by a script. Done 2026-10-31, Polish and English, for Darek as he will ship, with one paragraph saying the test build has only Home and Settings (see Before the next upload).
  • Capture the screenshots with scripts/screenshots.py from a release-shaped build, in every listing language, into art/play-screenshots/. Play wants at least two before the first review. Done 2026-10-31 as current-build placeholders: Home and Settings, light, in pl and en, from a local release build on the test phone (see Before the next upload). Re-shot 2026-11-01 for 0.3.0: Home with Słuchaj, the microphone disclosure, Listening and Settings, by hand (screencap + pad-screenshot.py --status-bar-px 75), because screenshots.py --locale needs API 33 and the test phone is API 30. Re-shot 2026-11-13 for phase 2 on the API 37 emulator from a release build: Home, the background disclosure, Listening, the notification (cropped to the card, so the shade’s system chrome stays out) and Settings with W tle. The Polish notification was re-shot the same day on the API 36 emulator with the system language Polish, so its header reads „Darek • teraz”.
  • Put the app on maksforge.com. Its icon goes at assets/icons/<repo>.webp in maks-sredniawa/maks-sredniawa.github.io, which the app’s own site loads, and the app goes on that home page. Replace description in docs/_config.yml too: it is the tagline under the app’s name.
  • Write the privacy policy and confirm maksforge.com/darek/privacy-policy serves it. Play requires a hosted URL, and an offline app has no server of its own. 2026-10-31: written for Darek as it will ship; Pages serves docs/ (both URL forms return 200). Ticked once the merge is read live at https://maksforge.com/darek/privacy-policy.
  • Answer the Console’s App content pages and write play-app-content.md in the same sitting — the reasons are in front of you then and gone a week later. It moves in the same commit as the privacy policy, every time. 2026-10-31: every page answered and recorded, the content rating last (submitted 21:43, IARC Completed: PEGI 3, ESRB Everyone, USK 0, Brazil 14+, In-app purchases). Sent for review 2026-10-31 with the listing and Alpha 0.2.0. Still open: re-read Target audience and content now that a rating exists, as play-app-content.md asks.
  • LICENSE and README.md. Bootstrap wrote a source-available notice and a Contributing that refuses pull requests, or with --licence reserved a notice that grants nothing and a placeholder. Keep both or replace both; they must agree.
  • Create the upload keystore, outside the repo, and put its four values in local.properties. Back it up somewhere that is not this machine: losing it means never being able to update the app on Play again. Fill in The upload key table in RELEASING.md the same day — the fingerprint is what Play asks you to compare. Done 2026-10-31: ~/.keystores/darek-upload.jks, table filled, the keystore and its password backed up off this machine.
  • Set up the GitHub repository — python3 scripts/repo-setup.py does the ruleset and the merge setting; the release-please PAT is the one step it cannot do, and without it no release PR is ever opened. Setting up a new repository in RELEASING.md. None of this is inherited from the template — GitHub copies files, never settings — and the build stays green while it is all still undone. Write the PAT’s expiry date into this item the day you create it: when it lapses the release PR simply stops appearing. Done 2026-10-31: ruleset, rebase-only merging and Pages by repo-setup.py; the PAT is set. Its expiry date is still owed here.
  • Set the five Play secrets and create the service account (docs/RELEASING.md) — the service account is the one step CI cannot do for itself. No account permissions; two app permissions, on this app only; no expiry date. Done 2026-10-31: darek-play-ci@darek-play.iam.gserviceaccount.com in Cloud project darek-play, invited with View app information and Release apps to testing tracks on Darek only; all six secrets set.
  • Plan the closed test. A new personal developer account reaches production only after a closed test — at the time of writing 12 testers opted in for 14 continuous days, then an access application. Recruit the testers before the build is ready, not after; publish-play-closed.yml moves each build to them. A tester-swap community, where developers test each other’s closed tests, filled the apps built from this template within days. Started 2026-10-31: Alpha 0.2.0 (versionCode 31) created in the Console, because the workflow cannot make a draft app’s first closed release (RELEASING.md), and in review. Testers: internal only-me-Tester, closed testers-community@googlegroups.com.
  • Recruiting the 12 testers for 14 continuous days is the owner’s routine. Not a task for this repo: once Alpha is approved you share the opt-in link in the developers’ tester-swap group (testers-community@googlegroups.com), where everyone tests each other’s apps. Settled 2026-10-31; the production-access application below follows once the Console shows the 14 days complete.
  • Decide the applicationId deliberately. It is fixed the moment the Play entry is created — not renameable, not transferable without losing every install and review. Decided 2026-10-31: com.maksforge.darek, as new MaksForge apps use the maksforge.com domain.

The tip — open until a real purchase is consumed

ADR-0007. The code is in; these are the parts only Play and a phone can prove. The steps are Selling the tip in RELEASING.md.

  • Keep it or delete it, before the first upload. Kept: it shipped with Alpha 0.2.0, and the rating’s Brazil 14+ came with it.
  • A build with billing on internal testing, then the four products created and active. Done 2026-11-13: tip_small … tip_xlarge, option tip, multi-quantity, Poland at 5/10/20/50 PLN.
  • One purchase from a licence tester on the internal install, ending in the thank-you line, and the same tip bought again. Not owed by Darek: TipJar and TipBilling are the shared code, and the purchase was proven once for every app on Binky (2026-11-13).
  • The debug build shows no tip section and no card. Play has never heard of the .debug package, so absent is the correct reading, not a failure.
  • Content rating re-submitted with digital purchases Yes, before production. Answered so in the first rating, 2026-10-31 (play-app-content.md, Content rating); phase F re-answers it for fuchy.

Before the next upload

Re-declarations a change owes Play, written down the day the change merges and ticked when the Console matches. A new permission, foreground service or data type is a Console change that waits in Publishing overview for Send for review with the upload that needs it. Empty most of the time.

  • In the release that ships phase 1’s checkpoint B (phase-1.md, Documents this phase amends): the listing’s test-build paragraph in both languages, the screenshots with Słuchaj, the privacy policy’s microphone bullet and its Text-to-speech paragraph (Darek’s own voice, ADR-0014), the listing’s text-to-speech paragraph in What it cannot do and play-app-content.md’s point 4 (the voice is no longer another app’s), RECORD_AUDIO’s row in play-app-content.md with the Data safety re-read, aab-permissions.py’s EXPECTED, and the licences of Vosk, its model, JNA, ONNX Runtime and the voice (with ADR-0014’s grey area). CALL_PHONE stays out of the release until phase 3. The repo half is done 2026-11-01 (listing, policy, play-app-content.md, screenshots; the allowlist and licences came with B). Tick when 0.3.0 is uploaded with its listing and the Console’s Data safety is confirmed unchanged.
  • In 0.9.0, phase 2’s release (phase-2.md). Done 2026-11-13: on internal as versionCode 158 (0.10.0 followed through CI the same day); the foreground-service declaration for microphone filed and published that day (play-app-content.md); the Console’s Data safety read back unchanged (doesn’t collect, doesn’t share, the policy URL); the listing’s full description and five screenshots per language sent for review.

Phase F — the weather fucha from the fuchy page

phase-F.md has the plan; tick a checkpoint when it merges with its readings.

  • A, the gate: Vosk’s vocabulary check through JNA (R1). Verdict 1, 2026-11-13.
  • B, fuchy as data, Darek’s own bundled. Done 2026-11-13; no reading, it is a refactor.
  • maks-sredniawa/darek-fuchy with its signing key and workflow, before C: C ships the public half. Its docs/_config.yml has remote_theme: maks-sredniawa/pages-theme, and the workflow writes the signed files and fuchy.json under docs/.
  • C, install from a file.
  • D, the network (R2), with README.md, docs/index.md, docs/_config.yml’s description, CLAUDE.md’s Stack row, SupportHandoff.kt’s KDoc, ADR-0007 and ManifestWiringTest rewritten to ADR-0008 the same day (phase-F.md, Documents this phase amends).
  • E, the weather (R3).
  • F, the page, updates and „Zgłoś fuchę” (R4–R6), with the listing, policy, Data safety and content rating in the same release.

Owed to the owner’s phone

  • Phase 2 on Android 16 and HyperOS (phase-2.md, The verdict): the gate reading, which Android 16’s emulator could not take, whether HyperOS kills the Listening service, autostart, and a night’s battery. Phase 2 stays unticked until they are read.

Owed before a Command that calls can fire

Phase D is ticked on what it built and on the owner’s voice; phase-D.md has what that reading showed. ADR-0016’s reading before she is trusted to call is still to do, and under either Persona the hold after the name (ADR-0009) has never been read against a Command that calls.

  • Before a Command that calls can fire under Daria, read her name and Decoys with GATE in the grammar, on the shipping grammar, against real voices, to phase 1’s bar, reworded for her. That is the probe’s Reading mode, which logs tokens, with the Decoys said in phrases, „Dario, otwórz bramę” among them. Her voice on the owner’s phone goes with it: load, synthesis time and the marker, from the say this field, no dial.
  • Before a Command that calls can fire under either Persona, read the hold on what it weakens (ADR-0009): on Home, the only place with the hold, with GATE in the shipping grammar, the Persona’s name, a 1 to 3 s gap, then „otwórz bramę” in a second voice. On the test phone, which has no SIM, so a fire is a reaction=GATE line in DarekListen and no call. The bar is phase 1’s: nothing fires. Whether a Command that calls gets the hold at all is the owner’s call on this reading.
  • Before a Command that calls can fire, finish R1 and take R2 (phase-1.md, The verdict). The owner stopped R1 on 2026-11-02. „Otwórz bramę” at 0.5 m read 8 of 9, so its bar is undecided; the 3 m and radio groups have no usable score; the noise group ran about 35 s of the script’s 4 minutes; and two false fires on „powiedz coś” are open, „Darek, powiedz mi coś” and one in noise, with no Decoys added or re-read. R2 stays open too: it was not taken, its phone is the only one that can dial, and verdict 1’s bar is both phones.

A known gap, with no reading to take: „Co tam, mordo?” and „Dziś 22°C…” still sound odd in her voice at every setting tried (D2). A better voice or a line of her own fixes it.

  • Add maks-forge-testers@googlegroups.com to Darek’s closed track (Alpha) in the Console: every closed track gets both groups (RELEASING.md, Testers), and Alpha was set up with testers-community@googlegroups.com alone. Saved and sent for review 2026-11-02.

After the closed test

  • The production-access application is in, with its answers recorded verbatim in play-app-content.md. Until it is granted the production track does not exist. The app this template was extracted from waited one day; that is one datapoint, not a promise. If it comes back rejected, read the reason before changing any artifact. Changing the build first leaves you arguing the reason against a different build.
  • Add the production countries in the Console — Test and release → Production → Countries / regions. A new app’s production track targets none, the API cannot add them, and a dry run does not catch it (RELEASING.md).
  • Decide the first production number. release-please never jumps to 1.0.0 by itself (with the major at 0 even feat!: bumps the minor), so it takes a Release-As: 1.0.0 footer (RELEASING.md), or a deliberate decision to stay on 0.x. In order: production access granted, then that version’s release notes in every locale on an ordinary branch (never on release-please’s own, which it force-pushes), then the AAB and the listing go up together.

Standing checks that never close

  • Every release: run the artifact checks on the built AAB, not on the source. aab-permissions.py is the one that finds what a dependency merged into your manifest — a permission you never declared, or a uses-feature that quietly filters the app off devices.
  • Every release: read the release notes gate’s output rather than trusting it passed.
  • Every Play declaration: confirm Data safety still holds against play-app-content.md, and write the date of the re-read here.
  • Every dependency bump: re-run licensee. The build fails on an unallowed licence, which is the point — but the fix is two things, the allowlist and the bundled text.
  • Test the release-shaped build on a device. A missing R8 keep rule does not crash; it makes a feature silently stop working.

Hi, I'm Maksymilian.

MaksForge is where I build the small tools I wish existed. Each one solves a single problem.