From 73e3faa24c6d139e4be4934ebae1267d89dea1e5 Mon Sep 17 00:00:00 2001
From: r-zakarya <82443831+r-zakarya@users.noreply.github.com>
Date: Sat, 22 Aug 2026 15:34:40 +0100
Subject: [PATCH] feat(android): a settings screen, and the unlock code can
finally be removed
Three things of different natures lived in collapsible panels on the home
screen - repository credentials, the unlock code, the log - on the one screen
where one mostly wants to open one's vault. They move to a settings sheet,
reachable from the list too.
A sheet, not a fourth view of the rail: each view costs its transform rules
and a place in back-key navigation, and settings is not somewhere one moves
about - it is a parenthesis one opens and closes.
The unlock code can now be REMOVED. It could not before: clearing the field
meant "nothing typed", not "drop the protection", so one could set a code and
never get out. It requires an open vault, and that is the proof being asked
for - the cache only let itself be read under the right code. Without that,
anyone holding the unlocked phone could lift the protection without knowing
it.
The refusal that guards every other path stays: it is what stopped a sync
from silently undoing a code. Removing is the one deliberate way out, and it
says so at the call site.
The old fields stay in the document as the source Delphi knows; the sheet is
only a view of them. Emptying them would lose the configuration at the first
sync.
---
android-client/PMA.Store.pas | 12 ++-
android-client/TESTS.md | 26 +++++-
android-client/web/app.html | 155 +++++++++++++++++++++++++++++++++--
mainApk.pas | 65 +++++++++++++++
4 files changed, 245 insertions(+), 13 deletions(-)
diff --git a/android-client/PMA.Store.pas b/android-client/PMA.Store.pas
index 4d19337..de6eb84 100644
--- a/android-client/PMA.Store.pas
+++ b/android-client/PMA.Store.pas
@@ -98,7 +98,8 @@ function HasVault: Boolean;
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
ARequireAuth: Boolean = False; AFreshKey: Boolean = False;
const APin: string = ''; const AKey: TBytes = nil;
- const ASalt: TBytes = nil; AIterations: Integer = 0);
+ const ASalt: TBytes = nil; AIterations: Integer = 0;
+ AClearPin: Boolean = False);
/// Le fichier local est-il scelle sous un code d'application ? Lu dans
/// l'en-tete, sans rien dechiffrer.
@@ -887,7 +888,7 @@ end;
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
ARequireAuth, AFreshKey: Boolean; const APin: string;
- const AKey, ASalt: TBytes; AIterations: Integer);
+ const AKey, ASalt: TBytes; AIterations: Integer; AClearPin: Boolean);
{$IF DEFINED(ANDROID)}
var
LWrap: TJSONObject;
@@ -908,7 +909,12 @@ begin
//
// Retirer le code demanderait donc un geste explicite, qui n'existe pas
// encore : aujourd'hui on le remplace en le retapant.
- if (APin = '') and VaultNeedsPin then
+ // AClearPin EST LA SEULE SORTIE, et elle se demande. Le refus ci-dessous
+ // protege tous les autres chemins - c'est lui qui a empeche une
+ // synchronisation de defaire un code en silence. Retirer un code reste
+ // possible, mais seulement quand quelqu'un l'a decide et vient de prouver
+ // qu'il le connaissait en ouvrant le coffre.
+ if (APin = '') and VaultNeedsPin and not AClearPin then
begin
FLastStoreError := 'cache sealed under an unlock code; refusing to ' +
'rewrite it without one';
diff --git a/android-client/TESTS.md b/android-client/TESTS.md
index 23b2505..9a54ff7 100644
--- a/android-client/TESTS.md
+++ b/android-client/TESTS.md
@@ -120,12 +120,32 @@ prouve rien.
côte à côte**. Ils demandent deux gestes différents ; les confondre dirait
« quelque chose ne va pas » sans dire quoi.
-### F. Pièces jointes
+### F. Réglages
+
+1. Roue dentée dans la liste, ou *Settings* sur l'accueil → l'écran s'ouvre.
+2. Les trois champs du dépôt doivent porter **les valeurs en place**, pas du
+ vide : ils ne sont qu'une vue des champs que Delphi connaît déjà.
+3. Modifier l'URL, revenir par la flèche, *Sync* → la nouvelle valeur sert.
+ C'est la contre-épreuve du report à la fermeture : un réglage modifié puis
+ refermé **sans quitter le champ** doit compter.
+4. Touche **retour** dans les réglages → elle referme **sans** verrouiller ni
+ revenir à la liste.
+5. **Poser un code** depuis les réglages : taper, *Set this code*, puis ouvrir
+ le coffre → *« Unlock code changed »*. Le sceau se pose toujours à
+ l'ouverture, et nulle part ailleurs.
+6. **Retirer le code** : *Remove the unlock code*, deux appuis → *« Unlock code
+ removed »*. Fermer l'application, rouvrir → **le code n'est plus demandé**.
+7. Contre-épreuve du retrait : verrouiller, puis essayer de retirer le code
+ sans avoir ouvert le coffre → refus *« Open the vault first »*. C'est ce qui
+ empêche quelqu'un tenant le téléphone déverrouillé de lever la protection
+ sans la connaître.
+
+### G. Pièces jointes
1. Ligne *Attachment* → flèche de téléchargement → le fichier apparaît dans
*Téléchargements* sous son nom, accents et arabe compris.
-### G. Code de déverrouillage
+### H. Code de déverrouillage
1. Poser un code, ouvrir le coffre, fermer, rouvrir → il est demandé.
2. Code faux → *« Wrong unlock code »*, et **Open vault reste offert** (le
@@ -135,7 +155,7 @@ prouve rien.
4. Contre-épreuve : taper n'importe quoi dans le champ du code puis *Sync* →
le code **ne doit pas changer**.
-### H. Empaquetage
+### I. Empaquetage
1. `build-apk.ps1 -Release -Keystore ... -Alias ...` → la sortie doit afficher
`Signer #1 certificate DN: CN=R Zakarya`. Si elle affiche `CN=Android
diff --git a/android-client/web/app.html b/android-client/web/app.html
index f9470ce..8033779 100644
--- a/android-client/web/app.html
+++ b/android-client/web/app.html
@@ -305,6 +305,20 @@
font: 11px/1.45 monospace; white-space: pre-wrap; word-break: break-all;
-webkit-user-select: text; user-select: text;
}
+ /* LES REGLAGES SONT UN VOILE, PAS UNE QUATRIEME VUE DU RAIL. Chaque vue du
+ rail coute ses regles de transformation et une place dans la navigation
+ par la touche retour ; les reglages ne sont pas un lieu ou l'on circule,
+ c'est une parenthese qu'on ouvre et qu'on referme. */
+ .sheet { position: fixed; inset: 0; z-index: 80; display: none;
+ flex-direction: column;
+ background: linear-gradient(var(--bg-top), var(--bg-bot)); }
+ body.on-settings .sheet { display: flex; }
+ .sheet h2 { margin: 18px 4px 6px; font-size: 13px; font-weight: 600;
+ color: var(--muted); text-transform: uppercase;
+ letter-spacing: .06em; }
+ .sheet .note { color: var(--muted); font-size: 13px; margin: 6px 4px 0;
+ line-height: 1.45; }
+
[hidden] { display: none !important; }
.toast {
@@ -363,8 +377,9 @@