Case study · Android

One photo. One sentence. One day.

Verified on deviceIn release validationNot published

Three One Frame screens on Android: today's frame, the archive, and the calendar

The constraint is the product.

One Frame accepts one photograph and one sentence a day. About twenty seconds. You can rewrite today as often as you like. You cannot fill in yesterday.

That refusal is the whole design. A journal you can backfill becomes a backlog, and a backlog becomes homework — the reason most journalling apps are abandoned in week three. A day you missed stays empty, and the calendar shows the gap rather than inviting you to repair it.

Local-first is the second refusal. Most apps that call themselves private still mean somebody else's server. There is no account here, no upload and no cloud backup — which also means uninstalling loses everything, so the app says exactly that on the first screen, before anyone has anything to lose.

What actually happens, in order.

The product is the sequence below, not the screens. Each row is behaviour someone can walk through on a phone today.

First launch
Three cards, then Start. They say what the app is (one frame a day, about twenty seconds), what it refuses (today only — a passed day cannot be filled in, and that is deliberate), and what the deal costs (no account, no server, no cloud backup; uninstalling loses everything, so export a backup yourself). The empty timeline then reads Today is worth keeping rather than showing zero of anything.
Creating today
Choose a frame — camera or library — and answer one prompt: What made today worth keeping? Both are required, and the save button says Save today or Save changes depending on which one you are doing. It stays above the keyboard, and it is inert until there is something to save. Leaving with unsaved work asks before discarding it.
Browsing back
The timeline gives today's photograph the whole screen, with the days gone by beneath it and a streak count. The archive is a grid of every frame. The calendar marks the days that have one, locks the future, and — every so often — the timeline surfaces one old day on its own: this day, a year ago.
Searching memory
One field over sentences and dates, counted in days rather than results. A new install that has never saved anything is told nothing to search yet, not nothing found — those are different facts, and only one of them is a failure.
Missing a day
Nothing happens. The day stays empty, the calendar shows the gap, and tapping it says so plainly: this day has passed and cannot be filled in. When a streak breaks the app offers to start again today instead of reporting a loss.
Reminders
One optional notification at a time you pick. It asks for the permission it needs, survives a reboot, and if the schedule cannot be set it says so and keeps your setting rather than silently reverting it.
Backup and export
One encrypted file through the system share sheet. You set a password, and the app explains what it is for: on this phone the backup restores without typing anything — the password is what opens it after the phone is reset, lost or replaced. It also says the part most apps leave out: forget it and nobody can recover it. Importing takes the same file back.
Background and privacy
Sent to the background, the journal is kept out of the task switcher — the app draws a photograph full bleed, so the recents snapshot would otherwise be the entry itself. An optional Privacy Lock asks for a fingerprint or the screen lock on the way in, and locks again immediately by default. The screen where you turn it on states that it is a screen in the way, not encryption.

Five decisions, and why.

No account

An account exists to identify you to a server. There is no server, so an account would collect an email address in order to protect nothing. The feature you do not ship cannot leak.

No server

A backend would add a breach surface, a subscription, a privacy policy with real teeth and an operating cost — to a product whose entire value is that today's photograph is on your phone and nowhere else. It would also make private a promise about somebody else's infrastructure rather than a fact about yours.

Local-first

Everything is on the device, which is a real trade and not a free win: uninstalling loses everything. The app is direct about that in onboarding and again in settings, and export exists so the trade is survivable.

Encrypted storage

Android already sandboxes every app. Encrypting on top of that is a second, narrower claim — that the journal database, each photograph and every exported backup are unreadable as files, with the keys held by the operating system rather than compiled into the app. It is worth claiming only because it was checked by reading the bytes back off a phone.

A deliberately small daily interaction

No streak pressure beyond a count, no prompts to catch up, no feed, no AI writing the sentence for you. The product asks for twenty seconds and then gets out of the way. Everything it refuses is a decision, not a gap in the roadmap.

141 tests pass. They could not see five of the defects that mattered.

The suite covers the domain rules, the storage boundary, the cipher framing, the backup envelope, migration ordering and a scan for untranslated copy. Every one of them passes, and the analyser reports nothing. Both are gates, not summaries.

A biometric prompt that never appeared. Authentication was wired correctly but hosted by the wrong activity class, and the platform declined to show anything at all. Nothing threw. Nothing logged. A security feature nobody has watched run has not been verified.

An archive of grey tiles. The photo loader was scoped to one route, so the archive — reached by a push — never received it. In a debug build that trips an assertion; in a release build the assertion is stripped and each tile is a plain grey box. A dead feature that looks like slow loading.

A backup that restored the words and lost the pictures. Photographs were copied into the archive still sealed with the exporting phone's key. On the phone that made the backup nothing looked wrong — which is the only phone the round-trip test ever used. Anywhere else, every restored photograph was a file nothing could open: exactly the case a backup exists for.

Photographs written around the storage layer, leaving plaintext files on the phone while every test stayed green — and a settings toggle that would not move, the smallest visible defect and the first one a person would have hit.

None of these were found by writing more tests. They were found by running the app on a physical phone, reading the files it had written, and watching a release build rather than a debug one. Emulators were excluded on purpose; so was the Raspberry Pi used to make the edit-and-look loop fast, because neither has a real Keystore or a real fingerprint sensor.

Evidence Each of the five is documented in the technical report with how it was detected, the fix, and the check that now fails without the fix.

Where this actually stands.

Verified on device
Encryption at rest, backup, restore onto a new key, task-switcher privacy and biometric unlock, all confirmed on a physical Redmi Note 10 Pro.
In release validation
A signed release artifact builds and installs. Store metadata, the version code and a permission declaration remain open, and several flows have not been re-run against the release build.
Not published
No Play Store listing, no store release, no production users and no uptime figure. There is no iOS device build and no TestFlight.

Read the technical report

Ask me anything about it.

huy.pkc@gmail.com LinkedIn GitHub

Back to portfolio