Eerste commit
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user