PMA.SyncClient now speaks two transports, told apart by the URL:
/api/v1/repos/ means the Gitea contents API, anything else the old PHP
endpoint. The PHP path stays until the Windows side publishes to the repo
too - without it the migration would need both ends flipped at once, with
no way back.
Three things differ on the Gitea side and nothing else: the header is
"token <PAT>" not "Bearer" (swapping them gives a silent 401), the body is
JSON carrying base64 content, and the fingerprint comparison happens in the
client since the API has no If-None-Match. The body is re-downloaded, which
is cheap; the four-second key derivation is still avoided. The result's
ETag field carries the sha, so the caller cannot tell which transport
answered.
usesCleartextTraffic is replaced by a network config that permits cleartext
to 127.0.0.1 and localhost BY NAME and forbids it everywhere else. The
attribute drew no such line: a mistyped URL sent the bearer token over the
network in the clear with nothing to signal it. Now it fails immediately,
which is the behaviour worth having.
Verified in the built APK: res/xml/network_security_config.xml is present
and the manifest attribute resolves to a real resource id. NOT verified
against a real repo - that needs a private repo, a published
vault-sync.json and a token, none of which exist yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Section 3 bis asked whether Delphi controls were needed at all and said to decide before investing further in the interface. It is decided, so it now records the answer and the two traps buried in the bridge - the iframe, and the page having to stay ASCII - rather than leaving them to be re-discovered.
Section 1 ter described Skia as the fix for Arabic; it is gone, and what survives of that episode is the packaging constraint, which still governs the build.
The table in section 0 claimed template, icons, attachments, timestamps and folders were unread. They are read now; it says which parts, and what is genuinely still missing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Contents API maps term for term onto what is already written: it returns the file with its sha and demands that sha to write, answering 409 on a stale one - exactly the roles of the ETag and the 412. The desktop's bounded reread-remerge-repush loop would work unchanged.
It also settles the one remaining obstacle: nothing to host, no certificate, outside access for free. Records the calls, and why the answer is a private Gitea rather than GitHub - git history never forgets, so every past snapshot stays readable under whatever sync password was current then. Survivable on your own disk, not on a third party's.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The theme still is not the one from the global search - the blue and the pill were taken, not the spacing or the density. And a larger one: whether there should be Delphi controls at all, when the desktop shows no native controls either and serves the same HTML in a WebView.
Worth deciding before investing further in the current interface: every display fix made today would be thrown away.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removing the screen lock permanently invalidates every Keystore key that requires authentication, so the cache was unreadable for good and re-downloading was the only way out. The automatic open kept the cache and reported; only the explicit tap cleared it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An Arabic attachment name displayed scrambled. Not a missed setting: FMX's original text engine does not shape complex scripts - no bidirectional reordering, no letter joining. Separating the size from the name limited the damage without removing it. Skia carries HarfBuzz and does the work.
GlobalUseSkia goes before Application.Initialize; the canvas is chosen at init and switching later does not reclaim what is already built. The price is libsk4d.so, 25 MB raw, and an APK from 13 to 22 MB.
Deploy rebuilds library\\lib just before calling aapt, so a hand-placed library disappears. A proper DeployFile entry was tried first - MSBuild ignores it, as it ignores every other one in this project. Hence repack-apk.ps1, which redoes aapt, zipalign and signing on the folder already prepared.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One attachment took the cache from 13 to 174 KB: the content travels base64 in the snapshot and is decrypted in memory with the rest. TVault reads name, type and size and DROPS content_b64 - holding it for the whole vault while nothing displays it would be paying for nothing.
The detail now says a document exists. Saving it is what remains, and what blocks that is not code but where a file lands on Android 10+. When that is settled, the content re-reads from the raw JSON.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Since Android 13 the system shows a preview of whatever was just copied, at the bottom of the screen. For a password that is precisely what we are trying to avoid: it appears in clear, outside the app, for several seconds. The flag that suppresses it is a ClipDescription extra, which IFMXClipboardService does not expose - hence going straight to ClipboardManager. Note Jcontent_ClipboardManager, not JClipboardManager: the latter is the deprecated android.text one, which knows only bare text and has no ClipData to carry an extra.
Backgrounding already locks; the idle timer covers the phone left face-up on a table with the app open. It samples timestamps every 15 s rather than counting seconds - a wake-up per second would cost battery for no precision that matters.
Attachments dropped from the plan: this vault has none.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Write with no prompt, read under the device credential, cache surviving a cold start with no derivation, and a silent 304. Records the three traps that hid behind it, none of which the compiler could see.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The launch-time auto-open received boUnavailable and wiped the cache, seconds after the write path had proved the device could authenticate. The diagnosis was wrong and the reaction irreversible. An automatic action now never clears the vault: it logs the cause and leaves the user on the unlock screen. Only an explicit tap on Open, followed by a clean refusal from the system, may clear it. PMA.Biometric keeps the reason in LastBiometricError, and a Java-layer exception now maps to boFailed rather than boUnavailable - an exception says nothing about the device. The log memo also wraps: it was cutting off exactly the end of the message, which is where the cause is named.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Works through the plan's §2 and §3 items that need no hardware and no
platform decision.
- allowBackup="false". The private directory holds vault.bin; a cloud
backup has no business in a password manager's path, even though what
would leave is encrypted by a key that stays in the chip.
- FLAG_SECURE, Release builds only - in debug it also blocks development
screenshots. Complementary to hiding on WillBecomeInactive rather than
redundant: that one covers the recents preview, the flag also covers
manual captures, screen recording and casting to an external display.
- List sorted by name (snapshot order is DB order, arbitrary to the eye),
entry icons decoded once per vault rather than on every keystroke, and a
folder dropdown whose selection survives a background refresh. No tag
filter: text search already matches tags.
- template and updated_at read and shown in the detail. updated_at is also
step 1 of the write path - nothing is arbitrable without it.
Android code is now type-checked without deploying: dccaarm64 reaches the
link step and fails only on the NDK's ldandroid.exe. A JNI declaration that
does not compile no longer waits for an APK to say so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The local vault now opens by itself at launch and the GET runs behind it,
silently. A 304 shows nothing; a network failure shows nothing either -
offline is a normal state, and an alert on every launch only teaches people
to dismiss alerts. Only a new snapshot manifests, by refreshing the list.
That silence is what puts the sync password inside the Keystore-encrypted
file next to the vault: without it a new snapshot cannot be opened, so every
edit made on the desktop would raise a password prompt at launch. The file
already holds every password in plaintext; this one adds almost nothing to
its exposure. A typed password still wins over the stored one, which is how
you fix one that no longer works.
Adopting a snapshot while the app is already open rebuilds the list in place
instead of navigating - mandatory even from the detail screen, since list
rows carry indices into the old vault and a tap would open the wrong entry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>