Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
PlatformSpecific.cs
1using System;
2using System.Text;
3using _Microsoft.Android.Resource.Designer;
4using Android;
5using Android.App;
6using Android.Content;
7using Android.Content.PM;
8//using Android.Gms.Extensions;
9using Android.Graphics;
10using Android.OS;
11using Android.Runtime;
12using Android.Views;
13using Android.Views.InputMethods;
14using AndroidX.Biometric;
15using AndroidX.Core.App;
16using AndroidX.Core.Graphics;
17using AndroidX.Core.View;
18using AndroidX.Fragment.App;
19using AndroidX.Lifecycle;
20using CommunityToolkit.Mvvm.Messaging;
21using Microsoft.Maui.Devices;
22
23//using Firebase.Messaging; // TODO: Firebase
24using Java.Util.Concurrent;
26using Plugin.Firebase.CloudMessaging;
27using Waher.Events;
29using Application = Android.App.Application;
30using FileProvider = AndroidX.Core.Content.FileProvider;
31using Resource = Android.Resource;
32using System.Globalization;
33
35{
40 {
41 private bool isDisposed;
42
47 {
48 this.InitializeKeyboard();
49 }
50
54 public void Dispose()
55 {
56 this.Dispose(true);
57 GC.SuppressFinalize(this);
58 }
59
63 protected virtual void Dispose(bool Disposing)
64 {
65 if (this.isDisposed)
66 return;
67
68 if (Disposing)
69 {
70 protectionTimer?.Dispose();
71 protectionTimer = null;
72 this.initializeKeyboardHandler?.RemoveCallbacksAndMessages(null);
73 this.initializeKeyboardHandler = null;
74 this.DetachInsetsListener();
75 this.rootView = null;
76 }
77
78 this.isDisposed = true;
79 }
80
81 private static bool screenProtected = true; // App started with screen protected.
82 private static Timer? protectionTimer = null;
83
87 public bool CanProhibitScreenCapture => true;
88
93 {
94 get => screenProtected;
95 set
96 {
97 try
98 {
99 protectionTimer?.Dispose();
100 protectionTimer = null;
101
102 if (screenProtected != value)
103 {
104 this.SetScreenSecurityProtection(value);
105 screenProtected = value;
106 }
107 }
108 catch (Exception Ex)
109 {
110 Log.Exception(Ex);
111 }
112 }
113 }
114
115 private void SetScreenSecurityProtection(bool Enabled)
116 {
117 MainThread.BeginInvokeOnMainThread(() =>
118 {
119 try
120 {
121 Activity? Activity = Platform.CurrentActivity;
122
123 if (Activity is not null)
124 {
125 if (Build.VERSION.SdkInt >= BuildVersionCodes.Tiramisu)
126 {
127#pragma warning disable CA1416
128 Activity.SetRecentsScreenshotEnabled(!Enabled);
129#pragma warning restore CA1416
130 }
131
132 if (Enabled)
133 {
134 Activity.Window?.SetFlags(WindowManagerFlags.Secure, WindowManagerFlags.Secure);
135 protectionTimer = new Timer(this.ProtectionTimerElapsed, null,
136 Constants.Security.MaxScreenRecordingTimeSeconds * 1000, Timeout.Infinite);
137 }
138 else
139 Activity.Window?.ClearFlags(WindowManagerFlags.Secure);
140 }
141 }
142 catch (Exception Ex)
143 {
144 Log.Exception(Ex);
145 }
146 });
147 }
148
149 private void ProtectionTimerElapsed(object? P)
150 {
151 MainThread.BeginInvokeOnMainThread(() => this.ProhibitScreenCapture = false);
152 }
153
157 public string? GetDeviceId()
158 {
159 try
160 {
161 // Try to get the device ID from SecureStorage first
162 string? DeviceId = SecureStorage.GetAsync("DeviceIdentifier").Result;
163
164 if (!string.IsNullOrEmpty(DeviceId))
165 {
166 return DeviceId; // Already stored, return it
167 }
168
169 // Otherwise, generate a new device ID
170 string? AndroidId = Android.Provider.Settings.Secure.GetString(
171 Android.App.Application.Context.ContentResolver,
172 Android.Provider.Settings.Secure.AndroidId);
173
174 // Optional: In rare cases, AndroidId can be null or unreliable on emulators. Fallback to a GUID if you prefer:
175 if (string.IsNullOrEmpty(AndroidId) || AndroidId == "9774d56d682e549c") // old bug: default bad ID
176 {
177 AndroidId = Guid.NewGuid().ToString();
178 }
179 // Store the device ID in SecureStorage for future use
180 SecureStorage.SetAsync("DeviceIdentifier", AndroidId).Wait();
181 return AndroidId;
182 }
183 catch (Exception ex)
184 {
185 // You may want to log ex.Message and ex.StackTrace here
186 try
187 {
188 App.SendAlertAsync($"Unable to get or store device ID: {ex.Message}", "text/plain").Wait();
189 this.CloseApplication().Wait();
190 }
191 catch (Exception)
192 {
193 System.Environment.Exit(0);
194 }
195 }
196
197 return null;
198 }
199
200
204 public Task CloseApplication()
205 {
206 Activity? Activity = Platform.CurrentActivity;
207 Activity?.FinishAffinity();
208
209 Java.Lang.JavaSystem.Exit(0);
210
211 return Task.CompletedTask;
212 }
213
221 public void ShareImage(byte[] PngFile, string Message, string Title, string FileName)
222 {
223 Context Context = Android.App.Application.Context;
224 Java.IO.File? ExternalFilesDir = Context.GetExternalFilesDir("");
225
226 if (ExternalFilesDir is null)
227 return;
228
229 if (!Directory.Exists(ExternalFilesDir.Path))
230 Directory.CreateDirectory(ExternalFilesDir.Path);
231
232 Java.IO.File FileDir = new(ExternalFilesDir.AbsolutePath + (Java.IO.File.Separator + FileName));
233
234 File.WriteAllBytes(FileDir.Path, PngFile);
235
236 Intent Intent = new(Intent.ActionSend);
237 Intent.PutExtra(Intent.ExtraText, Message);
238 Intent.SetType(Constants.MimeTypes.Png);
239
240 Intent.AddFlags(ActivityFlags.GrantReadUriPermission);
241 Intent.AddFlags(ActivityFlags.GrantWriteUriPermission);
242 Intent.PutExtra(Intent.ExtraStream, FileProvider.GetUriForFile(Context, "com.tag.IdApp.fileprovider", FileDir));
243
244 Intent? MyIntent = Intent.CreateChooser(Intent, Title);
245
246 if (MyIntent is not null)
247 {
248 MyIntent.AddFlags(ActivityFlags.NewTask);
249 Context.StartActivity(MyIntent);
250 }
251 }
252
253 /*
258 public Task<byte[]> CaptureScreen(int blurRadius)
259 {
260 blurRadius = Math.Min(25, Math.Max(blurRadius, 0));
261
262 Activity? Activity = Platform.CurrentActivity;
263 Android.Views.View? RootView = Activity?.Window?.DecorView.RootView;
264
265 if (RootView is null)
266 return Task.FromResult<byte[]>([]);
267
268 using Bitmap Screenshot = Bitmap.CreateBitmap(RootView.Width, RootView.Height, Bitmap.Config.Argb8888!);
269 Canvas Canvas = new(Screenshot);
270 RootView.Draw(Canvas);
271
272 Bitmap? Blurred = null;
273
274 if (Activity is not null && (int)Android.OS.Build.VERSION.SdkInt >= 17)
275 Blurred = ToBlurred(Screenshot, Activity, blurRadius);
276 else
277 Blurred = ToLegacyBlurred(Screenshot, blurRadius);
278
279 MemoryStream Stream = new();
280 Blurred.Compress(Bitmap.CompressFormat.Jpeg!, 80, Stream);
281 Stream.Seek(0, SeekOrigin.Begin);
282
283 return Task.FromResult(Stream.ToArray());
284 }
285
286 private static Bitmap ToBlurred(Bitmap originalBitmap, Activity? Activity, int radius)
287 {
288 // Create another bitmap that will hold the results of the filter.
289 Bitmap BlurredBitmap = Bitmap.CreateBitmap(originalBitmap);
290 RenderScript? RenderScript = RenderScript.Create(Activity);
291
292 // Load up an instance of the specific script that we want to use.
293 // An Element is similar to a C type. The second parameter, Element.U8_4,
294 // tells the Allocation is made up of 4 fields of 8 unsigned bits.
295 ScriptIntrinsicBlur? Script = ScriptIntrinsicBlur.Create(RenderScript, Android.Renderscripts.Element.U8_4(RenderScript));
296
297 // Create an Allocation for the kernel inputs.
298 Allocation? Input = Allocation.CreateFromBitmap(RenderScript, originalBitmap, Allocation.MipmapControl.MipmapFull,
299 AllocationUsage.Script);
300
301 // Assign the input Allocation to the script.
302 Script?.SetInput(Input);
303
304 // Set the blur radius
305 Script?.SetRadius(radius);
306
307 // Finally we need to create an output allocation to hold the output of the Renderscript.
308 Allocation? Output = Allocation.CreateTyped(RenderScript, Input?.Type);
309
310 // Next, run the script. This will run the script over each Element in the Allocation, and copy it's
311 // output to the allocation we just created for this purpose.
312 Script?.ForEach(Output);
313
314 // Copy the output to the blurred bitmap
315 Output?.CopyTo(BlurredBitmap);
316
317 // Cleanup.
318 Output?.Destroy();
319 Input?.Destroy();
320 Script?.Destroy();
321 RenderScript?.Destroy();
322
323 return BlurredBitmap;
324 }
325
326 // Source: http://incubator.quasimondo.com/processing/superfast_blur.php
327 public static Bitmap ToLegacyBlurred(Bitmap source, int radius)
328 {
329 Bitmap.Config? Config = source.GetConfig();
330 Config ??= Bitmap.Config.Argb8888; // This will support transparency
331
332 Bitmap? Img = source.Copy(Config!, true);
333
334 int w = Img!.Width;
335 int h = Img.Height;
336 int wm = w - 1;
337 int Hm = h - 1;
338 int wh = w * h;
339 int Div = radius + radius + 1;
340 int[] r = new int[wh];
341 int[] g = new int[wh];
342 int[] b = new int[wh];
343 int Rsum, Gsum, Bsum, x, y, i, P, P1, P2, yp, yi, yw;
344 int[] Vmin = new int[Math.Max(w, h)];
345 int[] Vmax = new int[Math.Max(w, h)];
346 int[] Pix = new int[w * h];
347
348 Img.GetPixels(Pix, 0, w, 0, 0, w, h);
349
350 int[] Dv = new int[256 * Div];
351 for (i = 0; i < 256 * Div; i++)
352 Dv[i] = (i / Div);
353
354 yw = yi = 0;
355
356 for (y = 0; y < h; y++)
357 {
358 Rsum = Gsum = Bsum = 0;
359 for (i = -radius; i <= radius; i++)
360 {
361 P = Pix[yi + Math.Min(wm, Math.Max(i, 0))];
362 Rsum += (P & 0xff0000) >> 16;
363 Gsum += (P & 0x00ff00) >> 8;
364 Bsum += P & 0x0000ff;
365 }
366 for (x = 0; x < w; x++)
367 {
368
369 r[yi] = Dv[Rsum];
370 g[yi] = Dv[Gsum];
371 b[yi] = Dv[Bsum];
372
373 if (y == 0)
374 {
375 Vmin[x] = Math.Min(x + radius + 1, wm);
376 Vmax[x] = Math.Max(x - radius, 0);
377 }
378
379 P1 = Pix[yw + Vmin[x]];
380 P2 = Pix[yw + Vmax[x]];
381
382 Rsum += ((P1 & 0xff0000) - (P2 & 0xff0000)) >> 16;
383 Gsum += ((P1 & 0x00ff00) - (P2 & 0x00ff00)) >> 8;
384 Bsum += (P1 & 0x0000ff) - (P2 & 0x0000ff);
385 yi++;
386 }
387 yw += w;
388 }
389
390 for (x = 0; x < w; x++)
391 {
392 Rsum = Gsum = Bsum = 0;
393 yp = -radius * w;
394 for (i = -radius; i <= radius; i++)
395 {
396 yi = Math.Max(0, yp) + x;
397 Rsum += r[yi];
398 Gsum += g[yi];
399 Bsum += b[yi];
400 yp += w;
401 }
402 yi = x;
403 for (y = 0; y < h; y++)
404 {
405 // Preserve alpha channel: ( 0xff000000 & pix[yi] )
406 int rgb = (Dv[Rsum] << 16) | (Dv[Gsum] << 8) | Dv[Bsum];
407 Pix[yi] = ((int)(0xff000000 & Pix[yi]) | rgb);
408 if (x == 0)
409 {
410 Vmin[y] = Math.Min(y + radius + 1, Hm) * w;
411 Vmax[y] = Math.Max(y - radius, 0) * w;
412 }
413 P1 = x + Vmin[y];
414 P2 = x + Vmax[y];
415
416 Rsum += r[P1] - r[P2];
417 Gsum += g[P1] - g[P2];
418 Bsum += b[P1] - b[P2];
419
420 yi += w;
421 }
422 }
423
424 Img.SetPixels(Pix, 0, w, 0, 0, w, h);
425 return Img;
426 }
427 */
428
433 {
434 get
435 {
436 try
437 {
438 if (!OperatingSystem.IsAndroidVersionAtLeast(23))
439 return false;
440
441
442 Context Context = Android.App.Application.Context;
443
444 // For Android 28 and later, check for UseBiometric; for earlier versions, check for UseFingerprint.
445 if (OperatingSystem.IsAndroidVersionAtLeast(28)) // API 28+
446 {
447 if (Context.CheckCallingOrSelfPermission(Manifest.Permission.UseBiometric) != Permission.Granted)
448 return false;
449 }
450 else
451 {
452 if (Context.CheckCallingOrSelfPermission(Manifest.Permission.UseFingerprint) != Permission.Granted)
453 return false;
454 }
455
456 BiometricManager Manager = BiometricManager.From(Context);
457 int Level = BiometricManager.Authenticators.BiometricWeak;
458
459 return Manager.CanAuthenticate(Level) == BiometricManager.BiometricSuccess;
460
461 // TODO: AndroidX package conflicts arose between Maui & Xamarin.AndroidX.Biometrics package, that the
462 // Plugin.Fingerprint seems to have resolved. Using this library while Maui fixes the problem, even
463 // though the library is not used in code.
464
465 // TODO: Consider alternative levels:
466 //
467 // public interface Authenticators {
468 // /**
469 // * Any biometric (e.g. fingerprint, iris, or face) on the device that meets or exceeds the
470 // * requirements for <strong>Class 3</strong> (formerly <strong>Strong</strong>), as defined
471 // * by the Android CDD.
472 // */
473 // int BIOMETRIC_STRONG = 0x000F;
474 //
475 // /**
476 // * Any biometric (e.g. fingerprint, iris, or face) on the device that meets or exceeds the
477 // * requirements for <strong>Class 2</strong> (formerly <strong>Weak</strong>), as defined by
478 // * the Android CDD.
479 // *
480 // * <p>Note that this is a superset of {@link #BIOMETRIC_STRONG} and is defined such that
481 // * {@code BIOMETRIC_STRONG | BIOMETRIC_WEAK == BIOMETRIC_WEAK}.
482 // */
483 // int BIOMETRIC_WEAK = 0x00FF;
484 //
485 // /**
486 // * The non-biometric credential used to secure the device (i.e. PIN, pattern, or password).
487 // * This should typically only be used in combination with a biometric auth type, such as
488 // * {@link #BIOMETRIC_WEAK}.
489 // */
490 // int DEVICE_CREDENTIAL = 1 << 15;
491 // }
492 }
493 catch (Exception)
494 {
495 return false;
496 }
497 }
498 }
506 {
507 // Biometric authentication requires at least API level 23.
508 if (OperatingSystem.IsAndroidVersionAtLeast(23))
509 return BiometricMethod.None;
510
511 Context Context = Android.App.Application.Context;
512
513 if (OperatingSystem.IsAndroidVersionAtLeast(28)) // API 28+
514 {
515 // Check for the UseBiometric permission (only available on API 28+)
516 if (Context.CheckCallingOrSelfPermission(Manifest.Permission.UseBiometric) != Permission.Granted)
517 {
518 return BiometricMethod.None;
519 }
520 }
521 else // For API levels 23 through 27
522 {
523 // Check for the UseFingerprint permission, which is available on earlier versions.
524 if (Context.CheckCallingOrSelfPermission(Manifest.Permission.UseFingerprint) != Permission.Granted)
525 {
526 return BiometricMethod.None;
527 }
528 }
529
530 BiometricManager Manager = BiometricManager.From(Context);
531 const int Level = BiometricManager.Authenticators.BiometricWeak;
532 return Manager.CanAuthenticate(Level) == BiometricManager.BiometricSuccess
533 ? BiometricMethod.Unknown
534 : BiometricMethod.None;
535 }
536
546 public async Task<bool> AuthenticateUserFingerprint(string Title, string? Subtitle, string Description, string Cancel,
547 CancellationToken? CancellationToken)
548 {
550 return false;
551
552 if (string.IsNullOrWhiteSpace(Title))
553 throw new ArgumentException("Title cannot be empty.", nameof(Title));
554
555 if (Platform.CurrentActivity is not FragmentActivity Activity)
556 return false;
557
558 try
559 {
560 BiometricPrompt.PromptInfo.Builder Builder = new();
561
562 Builder.SetAllowedAuthenticators(BiometricManager.Authenticators.BiometricWeak);
563
564 Builder.SetConfirmationRequired(false);
565 Builder.SetTitle(Title);
566 Builder.SetDescription(Description);
567 Builder.SetNegativeButtonText(Cancel);
568
569 if (!string.IsNullOrEmpty(Subtitle))
570 Builder.SetSubtitle(Subtitle);
571
572 BiometricPrompt.PromptInfo Prompt = Builder.Build();
573 IExecutorService? Executor = Executors.NewSingleThreadExecutor();
574 CallbackHandler Handler = new();
575 CancellationTokenRegistration? Registration = null;
576
577 BiometricPrompt Dialog = new(Activity, Executor, Handler);
578 try
579 {
580 Registration = CancellationToken?.Register(Dialog.CancelAuthentication);
581 Dialog.Authenticate(Prompt);
582
583 return await Handler.Result;
584 }
585 finally
586 {
587 if (Registration.HasValue)
588 Registration.Value.Dispose();
589
590 // Remove the lifecycle observer that is set by the BiometricPrompt.
591 // Reference: https://stackoverflow.com/a/59637670/1489968
592 // Review after referenced nugets (or Maui) has been updated.
593
594 Java.Lang.Class Class = Java.Lang.Class.FromType(Dialog.GetType());
595 Java.Lang.Reflect.Field[] Fields = Class.GetDeclaredFields();
596 Java.Lang.Reflect.Field? LifecycleObserver = Fields?.FirstOrDefault(f => f.Name == "mLifecycleObserver");
597
598 if (LifecycleObserver is not null)
599 {
600 LifecycleObserver.Accessible = true;
601 ILifecycleObserver? LastLifecycleObserver = LifecycleObserver.Get(Dialog).JavaCast<ILifecycleObserver>();
602 Lifecycle? Lifecycle = Activity.Lifecycle;
603
604 if (LastLifecycleObserver is not null && Lifecycle is not null)
605 Lifecycle.RemoveObserver(LastLifecycleObserver);
606 }
607
608 Dialog?.Dispose();
609 }
610 }
611 catch (Exception ex)
612 {
613 ServiceRef.LogService.LogException(ex);
614 return false;
615 }
616 }
617
618 private class CallbackHandler : BiometricPrompt.AuthenticationCallback, IDialogInterfaceOnClickListener
619 {
620 private readonly TaskCompletionSource<bool> result = new();
621
622 public Task<bool> Result => this.result.Task;
623
624 public override void OnAuthenticationSucceeded(BiometricPrompt.AuthenticationResult Result)
625 {
626 base.OnAuthenticationSucceeded(Result);
627 this.result.TrySetResult(true);
628 }
629
630 public override void OnAuthenticationError(int ErrorCode, Java.Lang.ICharSequence ErrorString)
631 {
632 base.OnAuthenticationError(ErrorCode, ErrorString);
633 this.result.TrySetResult(false);
634 }
635
636 public override void OnAuthenticationFailed()
637 {
638 base.OnAuthenticationFailed();
639 this.result.TrySetResult(false);
640 }
641
642 public void OnClick(IDialogInterface? Dialog, int Which)
643 {
644 this.result.TrySetResult(false);
645 }
646 }
647
652 public async Task<TokenInformation> GetPushNotificationToken()
653 {
654 string Token = string.Empty;
655
656 try
657 {
658 await CrossFirebaseCloudMessaging.Current.CheckIfValidAsync();
659 Token = await CrossFirebaseCloudMessaging.Current.GetTokenAsync();
660 }
661 catch (Exception ex)
662 {
663 ServiceRef.LogService.LogException(ex);
664 }
665
667 {
668 Token = Token,
669 ClientType = ClientType.Android,
670 Service = PushMessagingService.Firebase
671 };
672
673 return TokenInformation;
674 }
675
676 #region Keyboard
677
679 public event EventHandler<KeyboardSizeMessage>? KeyboardShown;
681 public event EventHandler<KeyboardSizeMessage>? KeyboardHidden;
688 public event EventHandler<KeyboardSizeMessage>? KeyboardSizeChanged;
689
690 private Activity? activity;
691 private Android.Views.View? rootView;
692 private double lastKeyboardHeight = 0;
693 private Handler? initializeKeyboardHandler;
694 private KeyboardInsetsListener? windowInsetsListener;
695
697 public void HideKeyboard()
698 {
699 if (this.activity is null || this.rootView is null)
700 return;
701 InputMethodManager? InputMethodManager = this.activity.GetSystemService(Context.InputMethodService) as InputMethodManager;
702 InputMethodManager?.HideSoftInputFromWindow(this.rootView.WindowToken, HideSoftInputFlags.None);
703 this.activity.Window?.DecorView.ClearFocus();
704 }
705
706 private void InitializeKeyboard()
707 {
708 try
709 {
710 this.initializeKeyboardHandler = new Handler(Looper.MainLooper!);
711 this.CheckRootView();
712 }
713 catch (Exception ex)
714 {
715 ServiceRef.LogService.LogException(ex);
716 }
717 }
718
719 private void CheckRootView()
720 {
721 this.activity = Platform.CurrentActivity;
722
723 Android.Views.View? currentRoot = this.activity?.Window?.DecorView?.RootView;
724 if (currentRoot is null)
725 {
726 this.initializeKeyboardHandler?.PostDelayed(this.CheckRootView, 100);
727 return;
728 }
729
730 if (ReferenceEquals(this.rootView, currentRoot) && this.windowInsetsListener is not null)
731 {
732 ViewCompat.RequestApplyInsets(this.rootView);
733 return;
734 }
735
736 this.DetachInsetsListener();
737
738 this.rootView = currentRoot;
739 this.windowInsetsListener = new KeyboardInsetsListener(this);
740 ViewCompat.SetOnApplyWindowInsetsListener(this.rootView, this.windowInsetsListener);
741 ViewCompat.RequestApplyInsets(this.rootView);
742 }
743
744 private void DetachInsetsListener()
745 {
746 if (this.rootView is not null)
747 {
748 ViewCompat.SetOnApplyWindowInsetsListener(this.rootView, null);
749 }
750
751 this.windowInsetsListener?.Dispose();
752 this.windowInsetsListener = null;
753 }
754
755 internal void ProcessWindowInsets(WindowInsetsCompat insets)
756 {
757 try
758 {
759 bool isImeVisible = insets.IsVisible(WindowInsetsCompat.Type.Ime());
760 AndroidX.Core.Graphics.Insets imeInsets = insets.GetInsets(WindowInsetsCompat.Type.Ime());
761 AndroidX.Core.Graphics.Insets systemInsets = insets.GetInsets(WindowInsetsCompat.Type.SystemBars());
762 int keyboardHeightPixels = isImeVisible ? Math.Max(0, imeInsets.Bottom - systemInsets.Bottom) : 0;
763 float keyboardHeightDip = ConvertToDip(keyboardHeightPixels);
764
765 if (isImeVisible && keyboardHeightDip > 0.5)
766 {
767 if (Math.Abs(this.lastKeyboardHeight - keyboardHeightDip) < 0.5)
768 return;
769
770 this.lastKeyboardHeight = keyboardHeightDip;
771 this.KeyboardSizeChanged.Raise(this, new KeyboardSizeMessage(keyboardHeightDip));
772 WeakReferenceMessenger.Default.Send(new KeyboardSizeMessage(keyboardHeightDip));
773 this.KeyboardShown.Raise(this, new KeyboardSizeMessage(keyboardHeightDip));
774 }
775 else
776 {
777 if (Math.Abs(this.lastKeyboardHeight) < 0.5)
778 return;
779
780 this.lastKeyboardHeight = 0;
781 this.KeyboardSizeChanged.Raise(this, new KeyboardSizeMessage(0));
782 WeakReferenceMessenger.Default.Send(new KeyboardSizeMessage(0));
783 this.KeyboardHidden.Raise(this, new KeyboardSizeMessage(0));
784 }
785 }
786 catch (Exception ex)
787 {
788 ServiceRef.LogService.LogException(ex);
789 }
790 }
791
792 private static float ConvertToDip(int pixelValue)
793 {
794 double density = DeviceDisplay.MainDisplayInfo.Density;
795 if (density <= 0)
796 density = 1;
797 return (float)(pixelValue / density);
798 }
799
800 private sealed class KeyboardInsetsListener : Java.Lang.Object, IOnApplyWindowInsetsListener
801 {
802 private readonly WeakReference<PlatformSpecific> ownerReference;
803
804 public KeyboardInsetsListener(PlatformSpecific owner)
805 {
806 this.ownerReference = new WeakReference<PlatformSpecific>(owner);
807 }
808
815 public WindowInsetsCompat? OnApplyWindowInsets(Android.Views.View? View, WindowInsetsCompat? Insets)
816 {
817 if (Insets is not null && this.ownerReference.TryGetTarget(out PlatformSpecific? Owner))
818 Owner.ProcessWindowInsets(Insets);
819 return Insets;
820 }
821 }
822 #endregion
823
824 #region Notifications
825 public void ShowMessageNotification(string Title, string MessageBody, IDictionary<string, string> Data)
826 {
827 Context Context = Application.Context;
828 Intent Intent = new Intent(Context, typeof(MainActivity));
829 Intent.AddFlags(ActivityFlags.ClearTop);
830 foreach (string Key in Data.Keys)
831 {
832 Intent.PutExtra(Key, Data[Key]);
833 }
834 PendingIntent? PendingIntent = Android.App.PendingIntent.GetActivity(Context, 100, Intent, PendingIntentFlags.OneShot | PendingIntentFlags.Immutable);
835 int ResIdentifier = Context.Resources?.GetIdentifier("app_icon", "drawable", Context.PackageName) ?? 0;
836 if (ResIdentifier == 0)
837 {
838 ServiceRef.LogService.LogWarning("App icon not found. Aborting local notification");
839 return;
840 }
841
842 if (Data.TryGetValue("fromJid", out string? FromJid) && !string.IsNullOrEmpty(FromJid))
843 {
844 Intent.SetData(Android.Net.Uri.Parse(Constants.UriSchemes.Xmpp + ":" + FromJid));
845 Intent.SetAction(Intent.ActionView);
846 }
847
848 NotificationCompat.Builder Builder = new NotificationCompat.Builder(Context, Constants.PushChannels.Messages)
849 .SetSmallIcon(ResIdentifier)
850 .SetContentTitle(Title)
851 .SetContentText(MessageBody)
852 .SetAutoCancel(true)
853 .SetContentIntent(PendingIntent);
854
855 NotificationManagerCompat NotificationManager = NotificationManagerCompat.From(Context);
856 NotificationManager.Notify(100, Builder.Build());
857 }
858
859 public void ShowIdentitiesNotification(string Title, string MessageBody, IDictionary<string, string> Data)
860 {
861 Context Context = Application.Context;
862 Intent Intent = new Intent(Context, typeof(MainActivity));
863
864 Intent.AddFlags(ActivityFlags.ClearTop);
865 foreach (string Key in Data.Keys)
866 {
867 Intent.PutExtra(Key, Data[Key]);
868 }
869
870 // Optionally add additional details (for example, appending a legal id)
871 string ContentText = MessageBody;
872 if (Data.TryGetValue("legalId", out string? LegalId) && !string.IsNullOrEmpty(LegalId))
873 {
874 Intent.SetData(Android.Net.Uri.Parse(Constants.UriSchemes.IotId + ":" + LegalId));
875 Intent.SetAction(Intent.ActionView);
876 ContentText += System.Environment.NewLine + $"({LegalId})";
877 }
878
879 PendingIntent? PendingIntent = Android.App.PendingIntent.GetActivity(Context, 101, Intent, PendingIntentFlags.OneShot | PendingIntentFlags.Immutable);
880
881 int ResIdentifier = Context.Resources?.GetIdentifier("app_icon", "drawable", Context.PackageName) ?? 0;
882 if (ResIdentifier == 0)
883 {
884 ServiceRef.LogService.LogWarning("App icon not found. Aborting local notification");
885 return;
886 }
887
888
889 NotificationCompat.Builder Builder = new NotificationCompat.Builder(Context, Constants.PushChannels.Identities)
890 .SetSmallIcon(ResIdentifier)
891 .SetContentTitle(Title)
892 .SetContentText(ContentText)
893 .SetAutoCancel(true)
894 .SetContentIntent(PendingIntent);
895
896 NotificationManagerCompat NotificationManager = NotificationManagerCompat.From(Context);
897 NotificationManager.Notify(101, Builder.Build());
898 }
899
900 public void ShowPetitionNotification(string Title, string MessageBody, IDictionary<string, string> Data)
901 {
902 Context Context = Application.Context;
903 Intent Intent = new Intent(Context, typeof(MainActivity));
904 Intent.AddFlags(ActivityFlags.ClearTop);
905 foreach (string Key in Data.Keys)
906 {
907 Intent.PutExtra(Key, Data[Key]);
908 }
909
910 // Use fromJid and rosterName to compose the notification body
911 string FromJid = Data.TryGetValue("fromJid", out string? Value) ? Value : string.Empty;
912 string RosterName = Data.TryGetValue("rosterName", out string? Value1) ? Value1 : string.Empty;
913 string ContentText = $"{(string.IsNullOrEmpty(RosterName) ? FromJid : RosterName)}: {MessageBody}";
914
915 PendingIntent? PendingIntent = Android.App.PendingIntent.GetActivity(Context, 102, Intent, PendingIntentFlags.OneShot | PendingIntentFlags.Immutable);
916 int ResIdentifier = Context.Resources?.GetIdentifier("app_icon", "drawable", Context.PackageName) ?? 0;
917 if (ResIdentifier == 0)
918 {
919 ServiceRef.LogService.LogWarning("App icon not found. Aborting local notification");
920 return;
921 }
922 NotificationCompat.Builder Builder = new NotificationCompat.Builder(Context, Constants.PushChannels.Petitions)
923 .SetSmallIcon(ResIdentifier)
924 .SetContentTitle(Title)
925 .SetContentText(ContentText)
926 .SetAutoCancel(true)
927 .SetContentIntent(PendingIntent);
928
929 NotificationManagerCompat NotificationManager = NotificationManagerCompat.From(Context);
930 NotificationManager.Notify(102, Builder.Build());
931 }
932
933 public void ShowContractsNotification(string Title, string MessageBody, IDictionary<string, string> Data)
934 {
935 Context Context = Application.Context;
936 Intent Intent = new Intent(Context, typeof(MainActivity));
937 Intent.AddFlags(ActivityFlags.ClearTop);
938
939 foreach (string Key in Data.Keys)
940 {
941 Intent.PutExtra(Key, Data[Key]);
942 }
943
944 StringBuilder ContentBuilder = new StringBuilder();
945 ContentBuilder.Append(MessageBody);
946 if (Data.TryGetValue("role", out string? Role) && !string.IsNullOrEmpty(Role))
947 {
948 ContentBuilder.AppendLine().Append(Role);
949 }
950 if (Data.TryGetValue("contractId", out string? ContractId) && !string.IsNullOrEmpty(ContractId))
951 {
952 ContentBuilder.AppendLine().Append(CultureInfo.InvariantCulture, $"({ContractId})");
953 }
954 if (Data.TryGetValue("legalId", out string? LegalId) && !string.IsNullOrEmpty(LegalId))
955 {
956 ContentBuilder.AppendLine().Append(CultureInfo.InvariantCulture, $"({LegalId})");
957 }
958
959 PendingIntent? PendingIntent = Android.App.PendingIntent.GetActivity(Context, 103, Intent, PendingIntentFlags.OneShot | PendingIntentFlags.Immutable);
960 int ResIdentifier = Context.Resources?.GetIdentifier("app_icon", "drawable", Context.PackageName) ?? 0;
961 if (ResIdentifier == 0)
962 {
963 ServiceRef.LogService.LogWarning("App icon not found. Aborting local notification");
964 return;
965 }
966 NotificationCompat.Builder Builder = new NotificationCompat.Builder(Context, Constants.PushChannels.Contracts)
967 .SetSmallIcon(ResIdentifier)
968 .SetContentTitle(Title)
969 .SetContentText(ContentBuilder.ToString())
970 .SetAutoCancel(true)
971 .SetContentIntent(PendingIntent);
972
973 NotificationManagerCompat NotificationManager = NotificationManagerCompat.From(Context);
974 NotificationManager.Notify(103, Builder.Build());
975 }
976
977 public void ShowEDalerNotification(string Title, string MessageBody, IDictionary<string, string> Data)
978 {
979 Context Context = Application.Context;
980 Intent Intent = new Intent(Context, typeof(MainActivity));
981 Intent.AddFlags(ActivityFlags.ClearTop);
982 foreach (string Key in Data.Keys)
983 {
984 Intent.PutExtra(Key, Data[Key]);
985 }
986
987 StringBuilder ContentBuilder = new StringBuilder();
988 ContentBuilder.Append(MessageBody);
989 if (Data.TryGetValue("amount", out string? Amount) && !string.IsNullOrEmpty(Amount))
990 {
991 ContentBuilder.AppendLine().Append(Amount);
992 if (Data.TryGetValue("currency", out string? Currency) && !string.IsNullOrEmpty(Currency))
993 ContentBuilder.Append(" " + Currency);
994 if (Data.TryGetValue("timestamp", out string? Timestamp) && !string.IsNullOrEmpty(Timestamp))
995 ContentBuilder.Append(string.Format(CultureInfo.InvariantCulture, " ({0})", Timestamp));
996 }
997
998 PendingIntent? PendingIntent = Android.App.PendingIntent.GetActivity(Context, 104, Intent, PendingIntentFlags.OneShot | PendingIntentFlags.Immutable);
999 int ResIdentifier = Context.Resources?.GetIdentifier("app_icon", "drawable", Context.PackageName) ?? 0;
1000 if (ResIdentifier == 0)
1001 {
1002 ServiceRef.LogService.LogWarning("App icon not found. Aborting local notification");
1003 return;
1004 }
1005 NotificationCompat.Builder Builder = new NotificationCompat.Builder(Context, Constants.PushChannels.EDaler)
1006 .SetSmallIcon(ResIdentifier)
1007 .SetContentTitle(Title)
1008 .SetContentText(ContentBuilder.ToString())
1009 .SetAutoCancel(true)
1010 .SetContentIntent(PendingIntent);
1011
1012 NotificationManagerCompat NotificationManager = NotificationManagerCompat.From(Context);
1013 NotificationManager.Notify(104, Builder.Build());
1014 }
1015
1016 public void ShowTokenNotification(string Title, string MessageBody, IDictionary<string, string> Data)
1017 {
1018 Context Context = Application.Context;
1019 Intent Intent = new Intent(Context, typeof(MainActivity));
1020 Intent.AddFlags(ActivityFlags.ClearTop);
1021 foreach (string Key in Data.Keys)
1022 {
1023 Intent.PutExtra(Key, Data[Key]);
1024 }
1025
1026 StringBuilder ContentBuilder = new StringBuilder();
1027 ContentBuilder.Append(MessageBody);
1028 if (Data.TryGetValue("value", out string? Value) && !string.IsNullOrEmpty(Value))
1029 {
1030 ContentBuilder.AppendLine().Append(Value);
1031 if (Data.TryGetValue("currency", out string? Currency) && !string.IsNullOrEmpty(Currency))
1032 ContentBuilder.Append(" " + Currency);
1033 }
1034
1035 PendingIntent? PendingIntent = Android.App.PendingIntent.GetActivity(Context, 105, Intent, PendingIntentFlags.OneShot | PendingIntentFlags.Immutable);
1036 int ResIdentifier = Context.Resources?.GetIdentifier("app_icon", "drawable", Context.PackageName) ?? 0;
1037 if (ResIdentifier == 0)
1038 {
1039 ServiceRef.LogService.LogWarning("App icon not found. Aborting local notification");
1040 return;
1041 }
1042 NotificationCompat.Builder Builder = new NotificationCompat.Builder(Context, Constants.PushChannels.Tokens)
1043 .SetSmallIcon(ResIdentifier)
1044 .SetContentTitle(Title)
1045 .SetContentText(ContentBuilder.ToString())
1046 .SetAutoCancel(true)
1047 .SetContentIntent(PendingIntent);
1048
1049 NotificationManagerCompat NotificationManager = NotificationManagerCompat.From(Context);
1050 NotificationManager.Notify(105, Builder.Build());
1051 }
1052
1053 public void ShowProvisioningNotification(string Title, string MessageBody, IDictionary<string, string> Data)
1054 {
1055 Context Context = Application.Context;
1056 Intent Intent = new Intent(Context, typeof(MainActivity));
1057 Intent.AddFlags(ActivityFlags.ClearTop);
1058 foreach (string Key in Data.Keys)
1059 {
1060 Intent.PutExtra(Key, Data[Key]);
1061 }
1062 PendingIntent? PendingIntent = Android.App.PendingIntent.GetActivity(Context, 106, Intent, PendingIntentFlags.OneShot | PendingIntentFlags.Immutable);
1063 int ResIdentifier = Context.Resources?.GetIdentifier("app_icon", "drawable", Context.PackageName) ?? 0;
1064 if (ResIdentifier == 0)
1065 {
1066 ServiceRef.LogService.LogWarning("App icon not found. Aborting local notification");
1067 return;
1068 }
1069 NotificationCompat.Builder Builder = new NotificationCompat.Builder(Context, Constants.PushChannels.Provisioning)
1070 .SetSmallIcon(ResIdentifier)
1071 .SetContentTitle(Title)
1072 .SetContentText(MessageBody)
1073 .SetAutoCancel(true)
1074 .SetContentIntent(PendingIntent);
1075
1076 NotificationManagerCompat NotificationManager = NotificationManagerCompat.From(Context);
1077 NotificationManager.Notify(106, Builder.Build());
1078 }
1079
1080 public Thickness GetInsets()
1081 {
1082 Activity? Activity = Platform.CurrentActivity;
1083 if (Activity?.Window?.DecorView is not Android.Views.View DecorView)
1084 return new Thickness(0);
1085
1086 float Density = Activity.Resources?.DisplayMetrics?.Density ?? 1f;
1087
1088
1089 if (OperatingSystem.IsAndroidVersionAtLeast(30)) // API 30+
1090 {
1091 WindowInsets? WindowInsets = DecorView.RootWindowInsets;
1092 if (WindowInsets is not null)
1093 {
1094 Android.Graphics.Insets Insets = WindowInsets.GetInsets(Android.Views.WindowInsets.Type.SystemBars());
1095 return new Thickness(
1096 Insets.Left / Density,
1097 Insets.Top / Density,
1098 Insets.Right / Density,
1099 Insets.Bottom / Density
1100 );
1101 }
1102 }
1103 else if (OperatingSystem.IsAndroidVersionAtLeast(23)) // API 23-29
1104 {
1105 WindowInsets? WindowInsets = DecorView.RootWindowInsets;
1106 if (WindowInsets is not null)
1107 {
1108 return new Thickness(
1109 WindowInsets.SystemWindowInsetLeft / Density,
1110 WindowInsets.SystemWindowInsetTop / Density,
1111 WindowInsets.SystemWindowInsetRight / Density,
1112 WindowInsets.SystemWindowInsetBottom / Density
1113 );
1114 }
1115 }
1116
1117 return new Thickness(0);
1118 }
1119
1120 #endregion
1121 }
1122}
Represents an instance of the Neuro-Access app.
Definition: App.xaml.cs:125
const string Png
The PNG MIME type.
Definition: Constants.cs:296
const string Provisioning
Provisioning channel
Definition: Constants.cs:769
const string Petitions
Petitions channel
Definition: Constants.cs:744
const string Identities
Identities channel
Definition: Constants.cs:749
const string Messages
Messages channel
Definition: Constants.cs:739
const string EDaler
eDaler channel
Definition: Constants.cs:759
const string Tokens
Tokens channel
Definition: Constants.cs:764
const string Contracts
Contracts channel
Definition: Constants.cs:754
Authentication constants
Definition: Constants.cs:50
const int MaxScreenRecordingTimeSeconds
Maximum number of seconds screen recording is allowed.
Definition: Constants.cs:89
const string Xmpp
XMPP URI Scheme (xmpp)
Definition: Constants.cs:188
const string IotId
The IoT ID URI Scheme (iotid)
Definition: Constants.cs:153
A set of never changing property constants and helpful values.
Definition: Constants.cs:24
Android implementation of platform-specific features.
EventHandler< KeyboardSizeMessage >? KeyboardShown
bool CanProhibitScreenCapture
If screen capture prohibition is supported
void ShareImage(byte[] PngFile, string Message, string Title, string FileName)
Shares an image in PNG format.
PlatformSpecific()
Android implementation of platform-specific features.
void HideKeyboard()
Force hide the keyboard
bool SupportsFingerprintAuthentication
If the device supports authenticating the user using fingerprints.
BiometricMethod GetBiometricMethod()
Gets the biometric method supported by the device. Currently on android, you cannot determine if the ...
async Task< bool > AuthenticateUserFingerprint(string Title, string? Subtitle, string Description, string Cancel, CancellationToken? CancellationToken)
Authenticates the user using the fingerprint sensor.
virtual void Dispose(bool Disposing)
IDisposable.Dispose
EventHandler< KeyboardSizeMessage >? KeyboardSizeChanged
Fired when the keyboard size changes.
string? GetDeviceId()
Gets the ID of the device
async Task< TokenInformation > GetPushNotificationToken()
Gets a Push Notification token for the device.
EventHandler< KeyboardSizeMessage >? KeyboardHidden
bool ProhibitScreenCapture
If screen capture is prohibited or not.
Thickness GetInsets()
Gets the safe area insets for the device (top, bottom, left, right).
Task CloseApplication()
Closes the application
Contains information about a push notification token.
Base class that references services in the app.
Definition: ServiceRef.cs:43
static ILogService LogService
Log service.
Definition: ServiceRef.cs:214
Static class managing the application event log. Applications and services log events on this static ...
Definition: Log.cs:14
static void Exception(Exception Exception, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, params KeyValuePair< string, object >[] Tags)
Logs an exception. Event type will be determined by the severity of the exception.
Definition: Log.cs:1657
Interface for platform-specific functions.
BiometricMethod
Enum representing the device biometric method for authentication.
class KeyboardSizeMessage(float KeyboardSize)
Keyboard size change message
Definition: Messages.cs:32
ClientType
Type of client requesting notification.
Definition: ClientType.cs:7