327 lines
18 KiB
Markdown
327 lines
18 KiB
Markdown
# 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:
|
|
|
|
```csharp
|
|
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
|
|
|
|
```powershell
|
|
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:
|
|
|
|
```powershell
|
|
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
|
|
|
|
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.
|