Files

21 KiB

De Android-app

De app is dezelfde applicatie als de website, met dezelfde schermen. Wat verschilt, is de weg naar de gegevens: de website praat rechtstreeks met MariaDB, de app praat via de SocketServer.

Hoe het in elkaar zit

Project Wat het is Te testen?
MijnBesluit.Client De aanroepen over de socket, met modellen voor de app. Geen regels, alleen vertalen. Ja — 10 controles in de zelftest, tegen een echte socketserver
MijnBesluit.AppUi De schermen (Blazor). Inloggen, overzicht, kluis, familie, momenten, nalatenschap. Compileert; het gedrag zit grotendeels in de laag eronder
MijnBesluit.App De Android-schil: één webview met de schermen erin. Nee — dit moet in Visual Studio op een toestel

De schermen staan bewust in een eigen project en niet in de app zelf. Zo kan de website ze later overnemen, en hoeft een wijziging maar op één plek.

Wat je nodig hebt in Visual Studio

De MAUI-werkbelasting (".NET Multi-platform App UI development"). Heb je die al voor RenHDashboardMaui, dan ben je er al. Het project staat op net10.0-android, net als dat project.

Klopt je versie niet, pas dan TargetFrameworks in src\MijnBesluit.App\MijnBesluit.App.csproj aan naar wat je wél hebt staan; de andere projecten hoeven niet mee te veranderen.

Eerste keer draaien

  1. Rechtermuis op MijnBesluit.App → Set as Startup Project.
  2. Kies bovenin een Android-emulator of je eigen toestel.
  3. F5.

Verschijnt bij het bouwen een melding dat Microsoft.AspNetCore.Components.WebView.Maui ontbreekt, dan levert jouw werkbelasting die niet vanzelf. Voeg hem dan toe via Manage NuGet Packages op MijnBesluit.App.

Waar de app verbinding mee zoekt

In MauiProgram.cs staat bovenaan:

public const string Server = "mijnbesluit.nl";
public const int Poort = 5200;
  • Op een emulator, met de server op je eigen pc: zet Server op 10.0.2.2 — dat is vanuit de emulator jouw pc — en zet tls: true om naar tls: false zolang er geen certificaat is.
  • Op een echt toestel: laat mijnbesluit.nl staan.

Een APK maken

D:\MijnBesluit VS\MijnBesluit\tools\Apk-maken.ps1 -Snel     # snel, om zelf te proberen
D:\MijnBesluit VS\MijnBesluit\tools\Apk-maken.ps1           # ondertekend, om weg te geven
D:\MijnBesluit VS\MijnBesluit\tools\Apk-maken.ps1 -Aab      # bundel voor de Play Store

De debug-versie is meteen te installeren op je eigen toestel (wel eerst "installeren uit onbekende bron" toestaan). Die wordt gebouwd met -p:EmbedAssembliesIntoApk=true: bij Debug laat MAUI de .NET-assemblies anders buiten het pakket, omdat Visual Studio ze bij het debuggen los naar het toestel duwt. Zonder die instelling installeert de APK wel, maar klapt de app bij het starten meteen dicht. De release-versie vraagt de eerste keer om een sleutelbestand aan te maken; dat komt in D:\MijnBesluitSleutels.

Bewaar dat sleutelbestand apart, net als de mastersleutel van de kluis. Android herkent een app aan de sleutel waarmee hij is ondertekend. Raak je hem kwijt, dan is er geen nieuwe versie meer uit te brengen die op de toestellen van je gebruikers over de oude heen gaat — ook niet met hulp van Google. Dan moet iedereen de app verwijderen en opnieuw installeren, en is alles wat lokaal op het toestel stond weg.

Voor de Play Store lever je geen APK aan maar een AAB (Android App Bundle): -Aab. Dat is dezelfde opdracht met een ander pakketformaat, hetzelfde sleutelbestand en hetzelfde wachtwoord. Google maakt uit die bundel per toestel een APK, zodat niemand de onderdelen voor andere toestellen meedownloadt.

Een AAB kun je niet zelf op een telefoon zetten. Voor het uitproberen en voor het rondsturen aan je eigen testers blijf je dus de APK gebruiken; de AAB is alleen voor de Play Console.

De serverkant

Staat klaar sinds 14 september 2026:

  • De SocketServer draait als Windows-service op mijnbesluit.nl, poort 5200.
  • Poort 5200 is open; Test-NetConnection mijnbesluit.nl -Port 5200 geeft True.
  • TLS 1.3, met het certificaat van mijnbesluit.nl uit de opslag WebHosting (B1BAFBB8A37A2899A2706FE037DDAB0F1694EFD1, geldig tot 1 april 2027).
  • Database en e-mail werken via de socket: SocketServer-proef.ps1 -Email <adres> levert een inlogcode in het postvak.

Controleer het met:

D:\MijnBesluit VS\MijnBesluit\tools\SocketServer-proef.ps1
D:\MijnBesluit VS\MijnBesluit\tools\SocketServer-proef.ps1 -Email jouw@adres.nl

Let op de vervaldatum van het certificaat: bij vernieuwing verandert de vingerafdruk en moet Socket:CertificateThumbprint mee, gevolgd door Restart-Service MijnBesluitSocket. De service zet bij het opstarten in het logboek in welke opslag hij het certificaat vond en tot wanneer het geldig is.

Wat de app op het toestel bewaart

Alleen het sessietoken, in de Keystore van Android (ToestelTokenopslag). Geen kluisinhoud, geen wachtwoorden: die worden opgehaald op het moment dat je ze bekijkt, en zijn weg zodra je het scherm sluit. Uitloggen wist het token van het toestel.

De terugknop van het toestel

Android heeft een eigen terugknop, en die sloot standaard de hele app — ook midden in een formulier. Dat loopt nu via Terugknop in MijnBesluit.AppUi.

Hoe het werkt: wie iets open heeft staan meldt zich daar aan en weer af zodra het dicht is. De laatste aanmelding is als eerste aan de beurt, dus in de volgorde waarin de dingen over elkaar heen liggen. In de praktijk:

  1. Staat er een foto open in een bijlagenvenster, dan gaat die dicht.
  2. Staat er een wijzigscherm open, dan gaat dat dicht — zonder op te slaan, net als Annuleren.
  3. Zit je in een scherm, dan ga je naar het overzicht.
  4. Ben je op het overzicht, dan verschijnt "Nog een keer terug om te sluiten". Pas een tweede druk binnen twee seconden sluit de app.

De schermen hoeven hier niets voor te doen: AppSchil regelt stap 3 en 4, en Popup regelt stap 1 en 2. Een scherm dat er later bij komt doet automatisch mee.

HoofdPagina.OnBackButtonPressed is het enige stukje MAUI erin: dat geeft de druk door en laat Android zijn gang gaan zodra niemand hem afhandelt.

Vanzelf op slot

Achter het tandwiel in de balk staat één instelling: na hoeveel minuten stilte de app dichtgaat. Uitlogklok houdt bij wanneer er voor het laatst iets gebeurde; de schil vangt elke aanraking en elke toets op — die bubbelen naar de buitenste laag, dus dat is één plek — en kijkt elke halve minuut of de termijn voorbij is.

Wat er dan gebeurt, hangt af van het toestel.

Toestel Wat er gebeurt Standaard
Heeft een schermslot, Android 10 of nieuwer De app gaat op slot. De sessie blijft staan; openen gaat met vingerafdruk, gezicht of de code van de telefoon. Aan, na 15 minuten — en "uit" bestaat daar niet
Geen schermslot, of ouder De app logt écht uit: het sessietoken gaat van het toestel af en er moet een nieuwe code per e-mail komen. Uit

Op een toestel met een schermslot kan de klok niet uit. Die keuze staat niet in de lijst, en een nul die er van vroeger nog in staat wordt opgetild naar het kwartier - in de klok zelf, niet in het scherm, zodat ook de schil ervan uitgaat. Het slot kost daar één aanraking; uit zetten levert dus bijna niets op en betekent wel dat een gevonden telefoon een open kluis is. Zonder schermslot ligt dat andersom en mag "uit" er wél staan.

Op slot gaat de lijn dicht. Zodra de app vergrendelt, verbreekt hij de verbinding met de SocketServer; na het ontgrendelen zet hij hem weer op. Het token blijft staan en de sessie aan de serverkant ook - die hangt aan het token en niet aan de verbinding - dus je bent gewoon weer waar je was.

Waarom dat de moeite is: de server verbreekt zelf een verbinding waarover tien minuten niets gebeurt, maar dat is een terugval, geen zekerheid. Een telefoon die in een tas verdwijnt of zijn netwerk kwijtraakt, laat een half-open socket achter tot die tien minuten om zijn. Zelf dichtdoen maakt er een zekerheid van, scheelt een plek van de vijfhonderd, en zorgt dat je na het ontgrendelen een verse verbinding hebt in plaats van een die misschien nog leeft.

Bij het openen vraagt hij altijd, en hij vraagt het aan de telefoon. Ligt er een sessie op dit toestel en kan het om een vingerafdruk vragen, dan komt dat venster - ongeacht de klok. De klok gaat over wat er gebeurt terwijl de app openstaat; dit gaat over het openen zelf. Zo is de app afsluiten en opnieuw starten geen manier om langs het slot te komen.

Let op waarop dat gebeurt: op wat er in de beveiligde opslag van de telefoon ligt, niet op een goedkeuring van de server (HeeftBewaardeSessieAsync, en BijHetOpenenAsync in de schil). Dat was eerst andersom, en dat was fout. Het slot ging pas op de app nadat de server het token had goedgekeurd, dus een telefoon zonder netwerk - of een server die net opnieuw was gestart - stuurde je naar het inlogscherm met een vraag om je e-mailadres, terwijl je telefoon prima wist wie je was. Nu komt eerst je vingerafdruk en pas daarna de vraag of de sessie nog loopt. Zolang de app dat nog niet weet, toont de schil alleen het logo: geen scherm dat een tel later achter een slot verdwijnt, en geen scherm dat alvast gegevens gaat ophalen.

Dat de sessie ook echt nog loopt, is de andere helft van hetzelfde verhaal: de SocketServer bewaart zijn sessies sinds db/16_sessies.sql in de database en niet meer alleen in het geheugen. Zonder dat logde elke uitrol of reboot van de server iedereen uit, en was de vingerafdruk niets waard. Zie docs/Socketprotocol.md, hoofdstuk Sessies.

Dat verschil zit in de prijs van vergeten. Met een toestelslot kost het je één aanraking, dus mag het standaard aan. Zonder kost het een e-mail afwachten, en dat leg je niemand ongevraagd op. Wie zélf "uit" kiest houdt uit: die nul staat dan in de opslag en is iets anders dan niets gekozen hebben. Er is een zelftest die dat verschil bewaakt.

IToestelslot vraagt het toestel om iemand te controleren; Toestelslot in MijnBesluit.App doet dat met BiometricPrompt en SetDeviceCredentialAllowed(true), zodat de code van de telefoon altijd als terugval werkt. De app krijgt alleen ja of nee terug. Er is geen eigen pincode, er wordt niets bewaard en er valt hier dus ook niets te lekken; de controle gebeurt in een apart stuk hardware waar geen enkele app bij kan.

SetDeviceCredentialAllowed staat vanaf Android 11 als verouderd gemarkeerd. De opvolger (SetAllowedAuthenticators) bestaat pas vanaf 11, en de app werkt vanaf 10. Zolang dat zo is hoort deze hier te staan; gaat de ondergrens ooit naar 11, dan mag het om.

Op slot laat de schil niets van de app meer zien — geen balk, geen navigatie, geen inhoud — en slikt de terugknop van het toestel. Een grendel die je nog door het raam laat kijken is geen grendel. Op het slotscherm staat ook "toch helemaal uitloggen", voor wie de telefoon uit handen geeft.

Twee dingen die verder met opzet zo zijn:

  • Er wordt op de wandklok gerekend, niet op een teller. Een telefoon die een uur in een tas zit telt dus net zo goed mee, ook al heeft de lus in de schil dan niet gedraaid. Bij het terugkomen uit de achtergrond seint App.xaml.cs de klok meteen in, vóór er gegevens worden opgehaald — dat is juist het geval waar dit voor bedoeld is. Stond de app al op slot, dan komt het venster van het toestel meteen weer in beeld.
  • De termijn staat in de gewone voorkeursopslag van Android (Toestelinstellingen), niet in de Keystore: daar hoort alleen in wat geheim is, en hoe lang de app wacht is dat niet. Het sessietoken staat er wél in (ToestelTokenopslag), en dat is ook precies wat achter dit slot blijft liggen terwijl de app dicht is.

Een sessie gaat er alleen af als de server hem weigert

Een app die op slot staat, staat op de achtergrond, en Android sluit zo'n app vaak gewoon af. Terugkomen is dan geen ontwaken maar een koude start: de app pakt het bewaarde token op en vraagt de server wie hij is. Op dat moment is het netwerk van de telefoon soms nog niet wakker.

Dat mislukken is geen reden om het token weg te gooien. Deed de app dat wel, dan moest je een code per e-mail aanvragen terwijl er niets mis was met je sessie — precies op het moment dat je dacht dat je alleen even je vinger hoefde te leggen. Alleen de server mag een sessie ongeldig verklaren (foutcode sessie-verlopen); dan ruimt SessieKwijtAsync het token op en gaat de app naar het inlogscherm. Blijft het token staan, dan blijft de app op zijn scherm met "geen verbinding" en probeert hij het bij de volgende handeling opnieuw.

De zelfcontrole "App: geen verbinding gooit het bewaarde token niet weg" houdt beide kanten vast.

De tegels in de lijsten

Momenten, Mijn mensen, Mijn kluis en Nalatenschap tonen allemaal dezelfde tegel:

  • Als er een profielfoto is, staat die vooraan. Anders de beginletters, even groot, zodat een lijst niet verspringt zodra er ergens een foto bij komt.
  • Drie regels ernaast, altijd in dezelfde opbouw:
    1. waar je op zoekt - het onderwerp, de naam, de titel. Loopt door tot de rand.
    2. de toelichting, met rechts eventueel een labeltje (de status bij Mijn mensen).
    3. de rest, met rechts de knoppen. Die regel mag leeg zijn; bij Mijn mensen is dat zo.
  • De tegel zelf aantikken opent hem. Bij Momenten en Nalatenschap klapt er iets uit, bij Mijn mensen en Mijn kluis opent het scherm met de gegevens. Daar staat dus geen knop voor.
  • Rechtsonder staan alleen de andere acties, als tekentje: de prullenbak, de envelop om iemand uit te nodigen. Een wijzertje (het driehoekje) laat zien dat er iets uitklapt; dat is geen knop. Elke knop heeft een title en een aria-label, dus een schermlezer noemt hem gewoon.

Het labeltje op regel 2 en de knoppen op regel 3 staan daardoor op elke tegel op dezelfde plek, pal boven elkaar. Dat is de reden dat de knoppen op de onderste regel staan en er niet overheen zweven: een knop is 40 pixels hoog, en dat is anderhalve tekstregel - hij kwam dus altijd over dat labeltje heen. De tegel is daar een pixel of vijftien hoger van (ongeveer 126 in plaats van 110), en niets loopt meer over elkaar.

Datums staan afgekort: 9 Jun. 2031. De maandnamen staan met de hand in Tonen en komen niet uit de taalinstelling van het toestel; op een telefoon die op Engels staat las de lijst anders "9 June 2031" terwijl de rest van het scherm Nederlands is.

Mijn kluis heeft bovenaan een keuzelijst om op soort te filteren, met de knop Toevoegen ernaast, net als op de website. Het filteren gebeurt bij de server (KluisAsync neemt een categorie mee) en niet in de lijst op het toestel: dan hoeft de telefoon niet alles op te halen om er drie dingen uit te laten zien.

Bij Mijn kluis is bekijken en wijzigen hetzelfde scherm. Er was eerder ook een uitklap onder de tegel die precies dezelfde gegevens en bijlagen toonde: twee plekken om te onderhouden en twee manieren om hetzelfde te doen.

Het logo staat op vier plekken, allemaal vanuit hetzelfde bestand:

Waar Bestand
Inlogscherm van de app, groot src/MijnBesluit.AppUi/wwwroot/logo.png
Merkteken in de balk van de app, klein hetzelfde bestand
Kop en inlogpagina van de website src/MijnBesluit.Web/wwwroot/logo.png
Pictogram en startscherm van de app Resources/AppIcon/appicon.png, Resources/Splash/splash.png

Het pictogram is alleen het witte woordmerk op een doorzichtige achtergrond, met het logogroen eronder via Color="#C1D13F" in het csproj. Zo maakt Android er zelf een rond, vierkant of druppelvormig pictogram van. Het woordmerk staat op zeventig procent van het vlak: alles buiten die cirkel mag een toestel wegsnijden, en op die maat blijft het hele woordmerk staan, hoe een telefoon het ook maskeert.

Op 26 pixels - het merkteken in de balk - is het woordmerk niet te lezen. Dat hoeft ook niet: daar staat de naam in letters ernaast. Groot en leesbaar staat het op het inlogscherm.

Nog niet gedaan: de melding op de telefoon gebruikt het app-pictogram als klein teken, en Android maakt daar een silhouet van. Een woordmerk wordt op 24 pixels een vlek. Een eigen, eenvoudig tekentje in Platforms/Android/Resources/drawable zou dat oplossen.

Wat er nog niet in zit

  • Meldingen op de telefoon als er een bericht bezorgd wordt — de SocketServer kan uit zichzelf een melding sturen, maar een push-melding als de app dicht is vraagt om Firebase
  • iOS: net10.0-ios toevoegen aan TargetFrameworks en een Mac om op te bouwen
  • Een grens aan hoe lang de app op slot mag blijven staan. Nu blijft de sessie liggen tot iemand hem opent of uitlogt; na een maand ongebruikt zou hij zichzelf kunnen opruimen.

Naar de Google Play Store

Stand op 16 september 2026. De eisen van Google wijzigen; kijk ze na op https://support.google.com/googleplay/android-developer.

Wat al klopt: de app bouwt op targetSdkVersion 36, en dat is sinds 31 augustus 2026 de eis voor nieuwe apps. De rechten zijn zuinig (internet, camera, media, vingerafdruk, meldingen) en de meldingstoestemming wordt op het moment zelf gevraagd.

Wat er is gebouwd omdat Google het eist:

  • Account opheffen, in de app én op /account-verwijderen zonder app. Zie docs/Beveiliging.md.
  • Een privacyverklaring op /privacy, met een link erheen vanaf het inlogscherm en vanuit Instellingen in de app.

Wat er nog moet:

  1. Een organisatie-account in de Play Console, op naam van het bedrijf - geen persoonlijk account. Persoonlijke accounts van na 13 november 2023 moeten eerst een gesloten test draaien met 12 testers die 14 dagen onafgebroken meedoen; organisatie-accounts niet. Een organisatie wordt wel geverifieerd (KvK, D-U-N-S), en dat duurt weken. Begin daarmee.
  2. Een AAB in plaats van een APK: dotnet publish -f net10.0-android -c Release -p:AndroidPackageFormat=aab
  3. Een upload-keystore, bewaard zoals de masterkey: apart, met back-up, buiten de projectmap. Zet Play App Signing aan; dan kan Google een nieuwe upload-sleutel geven als hij ooit weg is. Let op dat de app die gebruikers krijgen door Google opnieuw wordt ondertekend - voor de pushmeldingen maakt dat niets uit, maar alles wat aan een certificaatvingerafdruk hangt moet je met de sleutel van Google registreren.
  4. ApplicationVersion omhoog bij elke upload. Staat nu op 1.
  5. Het Data safety-formulier, de leeftijdsclassificatie en de winkelvermelding: icoon 512x512, banner 1024x500, minimaal twee schermafbeeldingen, korte en lange beschrijving, en het adres van de privacyverklaring.
  6. "Bevat advertenties" aanvinken zodra de advertentiebalk meedraait. De inhoud van die advertenties valt dan onder het beleid van Google; dat is een reden te meer om ze vooraf zelf goed te keuren.

Bouwen vanuit SideKick

In SideKick (project MijnBesluit) heet het onderdeel mijnbesluit-app, met Sleutel android. SideKick zoekt voor zo'n onderdeel eerst tools\Bouwen-<Sleutel>.ps1; dat is hier tools\Bouwen-android.ps1, dat tools\Apk-maken.ps1 aanroept en de ondertekende APK in D:\MijnBesluit VS\MijnBesluitpublish\android zet. Zonder dat script bouwt SideKick een Windows-programma (win-x64), en dat faalt op een Android-project met NU1102 ... Mono.win-x64.

Het wachtwoord van het sleutelbestand komt uit de gebruikersvariabele MIJNBESLUIT_KEYSTORE_WACHTWOORD; start SideKick opnieuw als je die net hebt gezet. Voor een AAB (Play Store) draai je Apk-maken.ps1 -Aab zelf.

Uploaden naar de Play Store vanuit SideKick (niet getest tegen Google)

SideKick heeft hiervoor een eigen Play Store-tegel bij Publiceren (het oude script Bouwen-playstore.ps1 en de rij mijnbesluit-playstore zijn vervangen en niet meer nodig; het script staat in _te_verwijderen). Zie Handleiding-voor-claude.md van SideKick, hoofdstuk 6c.

Kort: bij het onderdeel mijnbesluit-aab staat in Instellingen het vinkje Play aan (Track leeg = intern testen). De koppeling met Google Play vul je eenmalig in bij Instellingen > Algemeen > Winkelkoppelingen... (inhoud van het JSON-bestand van het serviceaccount plakken of het bestand kiezen; het komt alleen in secrets.json buiten git; Verbinding testen controleert het). Bij Publiceren: de AAB bouwen, dan Play-versie controleren (ApplicationVersion moet hoger zijn dan de hoogste versiecode in de Play Console) en Naar Play Store zetten. De release komt als concept op de track; publiceren doe je in de Play Console.

Release-opmerking: zet de tekst voor de release in docs\Play-release-opmerking.txt (UTF-8, maximaal 500 tekens, in het Nederlands, bijvoorbeeld "Nieuwe kluissoorten, beheerbaar via de website"). Jonathan bekijkt en wijzigt hem in SideKick met de knop Release-opmerking...; hij gaat mee met de upload. Pas hem aan voor elke nieuwe versie.

Eenmalig: de app staat in de Play Console met een eerste, met de hand geuploade AAB; het serviceaccount is uitgenodigd onder Gebruikers en machtigingen met het recht om releases te beheren.