Eerste commit

This commit is contained in:
jf
2026-09-27 14:21:31 +00:00
commit de4ff128cb
295 changed files with 47374 additions and 0 deletions
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true"
android:icon="@mipmap/appicon"
android:roundIcon="@mipmap/appicon_round"
android:supportsRtl="true">
<!-- Op welk kanaal een melding terechtkomt die binnenkomt terwijl de app dicht is. Zonder
deze regel maakt Firebase daar zelf een kanaal "Overig" voor aan, en staat dezelfde
soort melding dus op twee plekken in de instellingen van de telefoon. De naam hoort
gelijk te zijn aan Meldingsdienst.Kanaal. -->
<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="mijnbesluit-berichten" />
</application>
<!-- Om de app weer te openen nadat hij zichzelf heeft vergrendeld. Dit geeft geen toegang
tot vingerafdrukken: de app krijgt alleen ja of nee terug van het toestel. -->
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<!-- De app praat met de SocketServer. -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Camera en galerij, voor foto's bij een kluisitem en video's bij een moment.
Hier stond eerder bewust niets: hoe minder rechten, hoe minder er mis kan gaan. Maar
zonder CAMERA meldt Android dat het toestel niet kan opnemen, en verdwijnt de knop
"Video opnemen" uit de app zonder dat er iets misgaat of iets over te zien is.
Wat er NIET bij staat en ook niet bij hoort: locatie, contacten, en het schrijven naar
de opslag van de telefoon. We lezen alleen wat de gebruiker zelf aanwijst. -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- Voor Android 12 en ouder; vanaf 13 doen de twee regels hierboven het werk. -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- Niet verplicht: een toestel zonder camera mag de app gewoon installeren, dan valt
alleen de opnameknop weg. -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
</manifest>
@@ -0,0 +1,14 @@
using Android.App;
using Android.Content.PM;
using Android.OS;
namespace MijnBesluit.App;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
LaunchMode = LaunchMode.SingleTop,
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation |
ConfigChanges.UiMode | ConfigChanges.ScreenLayout |
ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
}
@@ -0,0 +1,24 @@
using Android.App;
using Android.Runtime;
namespace MijnBesluit.App;
[Application]
public class MainApplication : MauiApplication
{
public MainApplication(IntPtr handle, JniHandleOwnership ownership) : base(handle, ownership) { }
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
public override void OnCreate()
{
base.OnCreate();
#if FIREBASE
// Het meldingenkanaal moet er zijn voordat de eerste melding binnenkomt, ook als die
// binnenkomt terwijl de app dicht is. Hier en niet in de dienst zelf, want dit is het
// eerste wat draait.
Meldingsdienst.MaakKanaal(this);
#endif
}
}
@@ -0,0 +1,104 @@
#if FIREBASE
using Android.App;
using Android.Content;
using AndroidX.Core.App;
using Firebase.Messaging;
namespace MijnBesluit.App;
/// <summary>
/// Vangt meldingen op die binnenkomen terwijl de app openstaat.
///
/// Android zet een melding alleen zelf in de balk als de app op de achtergrond staat. Staat hij
/// open, dan geeft Android hem door aan deze dienst en gebeurt er verder niets - tenzij we hem
/// hier zelf tonen. Dat was precies het geval waarin de proefmelding "verstuurd" heette en er
/// op de telefoon niets te zien was.
///
/// De tekst komt uit het data-deel van het bericht en niet uit het notification-deel. Beide
/// staan erin (zie FirebasePush), maar het data-deel is het stuk waar de naamgeving in de
/// Android-binding niet per versie verschilt.
/// </summary>
[Service(Exported = false)]
[IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
public class Meldingsdienst : FirebaseMessagingService
{
/// <summary>
/// Het kanaal waarop deze meldingen binnenkomen. Vanaf Android 8 hoort elke melding bij een
/// kanaal, en kan de gebruiker per kanaal zeggen hoe opdringerig het mag zijn. De naam staat
/// ook in AndroidManifest.xml, zodat meldingen die binnenkomen terwijl de app dicht is op
/// hetzelfde kanaal terechtkomen en niet onder "Overig".
/// </summary>
public const string Kanaal = "mijnbesluit-berichten";
/// <summary>
/// Maakt het kanaal aan als het er nog niet is. Mag zo vaak aangeroepen worden als je wilt;
/// Android werkt een bestaand kanaal dan alleen bij. Gebeurt bij het starten van de app, want
/// een melding die binnenkomt terwijl de app dicht is heeft het kanaal al nodig.
/// </summary>
public static void MaakKanaal(Context context)
{
if (!OperatingSystem.IsAndroidVersionAtLeast(26)) return;
var kanaal = new NotificationChannel(Kanaal, "Berichten", NotificationImportance.High)
{
Description = "Laat weten dat er iets voor je klaarstaat. Wat erin staat komt er nooit bij te staan."
};
var beheer = (NotificationManager?)context.GetSystemService(Context.NotificationService);
beheer?.CreateNotificationChannel(kanaal);
}
public override void OnMessageReceived(RemoteMessage bericht)
{
var gegevens = bericht.Data;
gegevens.TryGetValue("titel", out var titel);
gegevens.TryGetValue("tekst", out var tekst);
if (string.IsNullOrWhiteSpace(titel)) titel = "MijnBesluit";
if (string.IsNullOrWhiteSpace(tekst)) tekst = "Er staat iets voor je klaar.";
Toon(this, titel, tekst);
}
/// <summary>
/// Firebase geeft een toestel af en toe een nieuw kenmerk. Het oude werkt dan niet meer.
/// De app meldt zich bij elke aanmelding opnieuw aan met het kenmerk van dat moment, dus dat
/// herstelt zichzelf zodra je weer inlogt. Zolang je ingelogd blijft kan er een melding
/// tussendoor vallen; dat is bewust niet hier opgelost, want hier is geen verbinding met de
/// server en geen sleutel om mee te praten.
/// </summary>
public override void OnNewToken(string kenmerk) => base.OnNewToken(kenmerk);
private static void Toon(Context context, string titel, string tekst)
{
MaakKanaal(context);
// Aantikken opent de app. SingleTop, zodat je niet een tweede keer hetzelfde scherm
// opent bovenop het scherm waar je al was.
var openen = new Intent(context, typeof(MainActivity));
openen.AddFlags(ActivityFlags.ClearTop | ActivityFlags.SingleTop);
var tik = PendingIntent.GetActivity(context, 0, openen,
PendingIntentFlags.UpdateCurrent | PendingIntentFlags.Immutable);
// ApplicationInfo.Icon en niet een eigen tekening: dat is een nummer dat Android zelf
// bijhoudt, dus hier hoeft geen tweede plaatje bijgehouden te worden dat kan ontbreken.
var melding = new NotificationCompat.Builder(context, Kanaal)
.SetContentTitle(titel)
.SetContentText(tekst)
.SetStyle(new NotificationCompat.BigTextStyle().BigText(tekst))
.SetSmallIcon(context.ApplicationInfo!.Icon)
.SetAutoCancel(true)
.SetPriority((int)NotificationPriority.High)
.SetContentIntent(tik)
.Build();
// Elk bericht een eigen nummer, anders vervangt de tweede melding de eerste.
NotificationManagerCompat.From(context).Notify(Volgnummer(), melding);
}
private static int _laatste;
private static int Volgnummer() => Interlocked.Increment(ref _laatste);
}
#endif
@@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "1047194010071",
"project_id": "mijnbesluit-f04b5",
"storage_bucket": "mijnbesluit-f04b5.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:1047194010071:android:815622efe5c7b59a084b92",
"android_client_info": {
"package_name": "nl.mijnbesluit.app"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyDPDKCc6tQm8Juh9LwvvWlxmUFTrMCU4So"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}
@@ -0,0 +1,13 @@
using Foundation;
namespace MijnBesluit.App;
/// <summary>
/// De iOS-kant van het opstarten. Tegenhanger van MainApplication op Android: hij doet niets
/// meer dan dezelfde MauiProgram aanroepen, want alles wat de app ís staat in gedeelde code.
/// </summary>
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<!-- 1 = iPhone, 2 = iPad. De schermen zijn gemaakt voor een telefoon, maar een iPad
weigeren levert alleen een gebruiker op die het niet snapt. -->
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
<!-- ==========================================================================
Waarvoor de app iets van het toestel vraagt.
Deze teksten zijn geen formaliteit. iOS laat ze letterlijk zien op het moment
dat de app het vraagt, en als de tekst ontbreekt sluit iOS de app ter plekke
af - geen foutmelding, hij is gewoon weg. Dat is de klassieke "hij doet het
op Android wel"-val.
Ze staan in het Nederlands omdat de app Nederlands is; iemand die hier "ja"
op moet zeggen, hoort te kunnen lezen waarvoor.
========================================================================== -->
<key>NSFaceIDUsageDescription</key>
<string>Om MijnBesluit te openen zonder telkens een code uit je mail te halen. Je gezicht wordt door je telefoon gecontroleerd; MijnBesluit krijgt het nooit te zien.</string>
<key>NSCameraUsageDescription</key>
<string>Om een foto of video te maken die je in je kluis bewaart of als bericht meestuurt.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Om het geluid op te nemen bij een videobericht.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Om een foto of document uit je fotobibliotheek toe te voegen aan je kluis of aan een bericht.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Om een foto uit MijnBesluit op te slaan in je fotobibliotheek.</string>
<!-- ==========================================================================
BEWUST NOG NIET INGEVULD: ITSAppUsesNonExemptEncryption.
Apple vraagt bij elke inzending of de app encryptie gebruikt die buiten de
vrijstellingen valt. Zet je hier `false`, dan verklaar je dat dat niet zo is -
en dat is een verklaring met juridisch gewicht, geen vinkje. Deze app
versleutelt een kluis met gegevens, dus dat antwoord is niet vanzelfsprekend
"nee" zoals bij een app die alleen HTTPS gebruikt.
Laat dit staan zoals het is: zonder deze sleutel vraagt App Store Connect het
bij elke inzending, en dan beantwoord je het bewust. Wil je er vanaf, laat het
dan eerst nakijken door iemand die deze regels kent, en zet het antwoord er
daarna bij met de datum en de reden erbij in dit bestand.
Wat hier NIET hoort: een uitzondering op App Transport Security. De verbinding
met de SocketServer is een eigen TLS-socket en geen URL-verkeer, dus ATS gaat
er niet over. Een uitzondering toevoegen die je niet nodig hebt, levert alleen
vragen op bij de beoordeling.
========================================================================== -->
</dict>
</plist>
@@ -0,0 +1,14 @@
using ObjCRuntime;
using UIKit;
namespace MijnBesluit.App;
/// <summary>Het startpunt op iOS. Android heeft dit niet nodig; daar start Android de Activity.</summary>
public class Program
{
private static void Main(string[] args)
{
// Vanaf hier neemt UIKit het over en roept het AppDelegate aan.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}