fix(android): report the biometric failure instead of destroying the cache

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>
This commit is contained in:
r-zakarya
2026-08-15 13:42:07 +01:00
parent 5187da3070
commit 09048410e1
4 changed files with 164 additions and 10 deletions
+56 -6
View File
@@ -22,12 +22,26 @@ uses
System.SysUtils;
type
/// boUnavailable : l'appareil ne PEUT PAS authentifier - aucune empreinte,
/// aucun code. La cle sous authentification est alors definitivement
/// inutilisable, et le cache qu'elle protege aussi.
/// boFailed : la tentative a echoue pour une autre raison, y compris une
/// exception de la couche Java. Ne dit RIEN sur l'appareil, donc ne
/// justifie jamais de detruire quoi que ce soit.
TBioOutcome = (boOK, boUnavailable, boFailed, boCancelled);
/// L'appareil peut-il authentifier (empreinte, visage, ou code de
/// verrouillage) ? False signifie qu'aucune protection n'est configuree.
function BiometricAvailable: Boolean;
/// Pourquoi la derniere tentative a echoue. Vide apres un succes.
///
/// Existe parce qu'un echec avale sinon sa propre cause : un TBioOutcome ne
/// distingue pas « l'appareil n'a aucune protection » de « la couche Java a
/// leve quelque chose », et l'appelant prend des decisions opposees dans les
/// deux cas - dont celle d'effacer le cache.
function LastBiometricError: string;
/// Demande l'authentification puis appelle AThen. Non bloquant : le resultat
/// arrive par le rappel, sur le thread principal.
/// Si l'appareil ne sait pas authentifier, AThen est appele avec boUnavailable
@@ -37,10 +51,22 @@ procedure AuthenticateThen(const APrompt: string; const AThen: TProc<TBioOutcome
implementation
// La clause uses de l'implementation doit venir AVANT toute declaration :
// placee apres, Delphi ne s'en plaint qu'a la derniere ligne du fichier.
{$IF DEFINED(ANDROID)}
uses
FMX.BiometricAuth, FMX.Platform;
{$ENDIF}
var
FLastError: string = '';
function LastBiometricError: string;
begin
Result := FLastError;
end;
{$IF DEFINED(ANDROID)}
const
// DeviceCredential inclus : un telephone protege par un schema ou un code,
// sans empreinte enrolee, doit rester utilisable. C'est le cas courant.
@@ -74,10 +100,22 @@ begin
// de repondre False. Sans cette garde, un manifeste incomplet ne se traduit
// pas par « pas de biometrie » mais par une boite d'exception en pleine
// ouverture du coffre.
FLastError := '';
try
Result := AcquireService(LSvc) and LSvc.CanAuthenticate;
if not AcquireService(LSvc) then
begin
FLastError := 'service biometrique absent de la plateforme';
Exit(False);
end;
Result := LSvc.CanAuthenticate;
if not Result then
FLastError := 'CanAuthenticate: aucune protection utilisable';
except
Result := False;
on E: Exception do
begin
FLastError := E.ClassName + ': ' + E.Message;
Result := False;
end;
end;
end;
{$ELSE}
@@ -95,20 +133,28 @@ begin
if not Assigned(AThen) then Exit;
// AcquireService pose les forces ; CanAuthenticate n'a de sens qu'apres.
FLastError := '';
if not AcquireService(LSvc) then
begin
AThen(boUnavailable);
FLastError := 'service biometrique absent de la plateforme';
AThen(boFailed); // pas boUnavailable : on ne sait rien de l'appareil
Exit;
end;
try
if not LSvc.CanAuthenticate then
begin
// Reponse NETTE du systeme : l'appareil n'a rien pour authentifier.
FLastError := 'CanAuthenticate: aucune protection utilisable';
AThen(boUnavailable);
Exit;
end;
except
AThen(boUnavailable);
Exit;
on E: Exception do
begin
FLastError := E.ClassName + ': ' + E.Message;
AThen(boFailed);
Exit;
end;
end;
LProps := Default(TBiometricPromptProperties);
@@ -134,7 +180,11 @@ begin
AThen(boFailed);
end);
except
AThen(boUnavailable);
on E: Exception do
begin
FLastError := E.ClassName + ': ' + E.Message;
AThen(boFailed);
end;
end;
end;
{$ELSE}