using MijnBesluit.AppUi;
#if ANDROID
using Android.App;
using Android.Content;
using Android.Hardware.Biometrics;
using Android.OS;
#elif IOS
using LocalAuthentication;
#endif
namespace MijnBesluit.App;
///
/// Vraagt het toestel om iemand te controleren: vingerafdruk, gezicht, of de pincode of het
/// patroon van de telefoon zelf.
///
/// Wat hier NIET gebeurt: de app vraagt geen eigen pincode en bewaart er ook geen. Er is dus
/// niets bij te houden, niets te vergeten en niets te lekken. De telefoon doet de controle in
/// een apart stuk hardware waar geen enkele app bij kan, en geeft alleen ja of nee terug.
///
/// Vanaf Android 10. Daarvoor bestaat dit venster nog niet in de vorm waarin het ook de code
/// van de telefoon accepteert, en dan zou een vingerafdrukloze telefoon er niet meer in komen.
/// Op oudere toestellen is dus onwaar en logt de app uit zoals eerst.
///
/// Op iOS doet LocalAuthentication hetzelfde, met dezelfde afspraak: Face ID of Touch ID, en
/// de toestelcode als terugval. Dat laatste zit daar in de keuze van het beleid
/// (DeviceOwnerAuthentication) in plaats van in een aparte schakelaar.
///
public class Toestelslot : IToestelslot
{
#if ANDROID
public bool Beschikbaar
{
get
{
// Zonder schermslot heeft vergrendelen geen zin: dan komt iedereen die de telefoon
// in handen heeft er met één veeg weer in, en is uitloggen de enige echte grendel.
if (!OperatingSystem.IsAndroidVersionAtLeast(29)) return false;
var bewaking = (KeyguardManager?)Platform.AppContext.GetSystemService(Context.KeyguardService);
return bewaking?.IsDeviceSecure == true;
}
}
public Task OntgrendelAsync(string titel, string uitleg)
{
if (!Beschikbaar) return Task.FromResult(false);
var activiteit = Platform.CurrentActivity;
if (activiteit is null) return Task.FromResult(false);
var klaar = new TaskCompletionSource();
try
{
// SetDeviceCredentialAllowed is vanaf Android 11 als verouderd gemarkeerd, maar het
// is de enige manier die vanaf Android 10 werkt én de code van de telefoon als
// terugval toelaat. De opvolger (SetAllowedAuthenticators) bestaat pas vanaf 11.
// Zolang de app vanaf 10 werkt, hoort dit hier te staan.
var venster = new BiometricPrompt.Builder(activiteit)
.SetTitle(titel)
.SetDescription(uitleg)
.SetDeviceCredentialAllowed(true)
.Build();
// Geen annuleersein dat we zelf afbreken: het venster heeft zijn eigen knop, en de
// uitkomst daarvan komt via OnAuthenticationError binnen.
venster.Authenticate(new CancellationSignal(), activiteit.MainExecutor!, new Antwoord(klaar));
}
catch (Exception)
{
// Een toestel dat hier struikelt hoort de app niet te laten vallen. Het slot blijft
// dan dicht en je kunt nog altijd helemaal uitloggen.
klaar.TrySetResult(false);
}
return klaar.Task;
}
/// Wat het venster terugmeldt.
private sealed class Antwoord : BiometricPrompt.AuthenticationCallback
{
private readonly TaskCompletionSource _klaar;
public Antwoord(TaskCompletionSource klaar) => _klaar = klaar;
public override void OnAuthenticationSucceeded(BiometricPrompt.AuthenticationResult? uitkomst) =>
_klaar.TrySetResult(true);
/// Afgebroken, te vaak mis, of het toestel kan het niet. Hoe dan ook: niet open.
public override void OnAuthenticationError(BiometricErrorCode code, Java.Lang.ICharSequence? tekst) =>
_klaar.TrySetResult(false);
///
/// Eén poging die niet klopte - een vinger die verkeerd lag. Het venster blijft staan en
/// laat het opnieuw proberen, dus hier hoort niets te gebeuren. Pas als het toestel er
/// mee ophoudt komt OnAuthenticationError.
///
public override void OnAuthenticationFailed() { }
}
#elif IOS
public bool Beschikbaar
{
get
{
// DeviceOwnerAuthentication en niet DeviceOwnerAuthenticationWithBiometrics: het
// eerste is "Face ID of Touch ID, en anders de code van de telefoon", het tweede
// eist een vingerafdruk of gezicht en niets anders. Dat laatste zou betekenen dat
// een toestel met alleen een cijfercode er niet meer in komt - precies de gebruiker
// die we op Android ook niet buitensluiten.
using var vraag = new LAContext();
return vraag.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthentication, out _);
}
}
public async Task OntgrendelAsync(string titel, string uitleg)
{
// iOS toont één regel tekst en geen kop, dus de titel valt hier weg. Wat er in beeld
// komt is de uitleg; die hoort dus op zichzelf te kunnen staan.
//
// Let op: staat NSFaceIDUsageDescription niet in Info.plist, dan sluit iOS de app hier
// ter plekke af - geen foutmelding, hij is weg. Zie Platforms/iOS/Info.plist; er is een
// zelfcontrole die daarop let.
using var vraag = new LAContext();
if (!vraag.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthentication, out _)) return false;
// Afgebroken, te vaak mis, of het toestel kan het niet: dan komt hier onwaar uit en
// blijft het slot dicht. De fout zelf laten we liggen - er valt voor de gebruiker niets
// mee te doen, en op het slotscherm staat al wat hij nog wél kan.
var (gelukt, _) = await vraag.EvaluatePolicyAsync(LAPolicy.DeviceOwnerAuthentication, uitleg);
return gelukt;
}
#else
public bool Beschikbaar => false;
public Task OntgrendelAsync(string titel, string uitleg) => Task.FromResult(false);
#endif
}