using System; using System.Collections.Generic; using System.Linq; using Android.App; using Android.Content; using Android.Graphics; using Android.Hardware.Camera2; using Android.Media; using Android.OS; using Android.Views; using Android.Widget; using ZXing; using ZXing.Common; using AndroidResult = Android.App.Result; namespace PalladiumWallet.Mobile; /// /// Full-screen activity for QR scanning via Camera2 + ZXing.Net. /// Returns to MainActivity via SetResult with the extra "qr" = code text. /// [Activity(Theme = "@style/MyTheme.NoActionBar", ScreenOrientation = Android.Content.PM.ScreenOrientation.Portrait)] internal sealed class ScannerActivity : Activity, TextureView.ISurfaceTextureListener { internal const string ResultKey = "qr"; private const int PermReq = 1; private HandlerThread? _bgThread; private Handler? _bgHandler; private CameraDevice? _camera; private CameraCaptureSession? _session; private ImageReader? _imageReader; private SurfaceTexture? _surfaceTex; private bool _permOk; private volatile bool _done; private long _lastDecode; protected override void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); Window!.SetFlags(WindowManagerFlags.Fullscreen, WindowManagerFlags.Fullscreen); var root = new FrameLayout(this); var preview = new TextureView(this) { SurfaceTextureListener = this }; root.AddView(preview, new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MatchParent, FrameLayout.LayoutParams.MatchParent)); var hint = new TextView(this) { Text = "Inquadra un codice QR" }; hint.SetTextColor(Color.White); hint.SetBackgroundColor(Color.ParseColor("#AA000000")); hint.SetPadding(24, 12, 24, 12); root.AddView(hint, new FrameLayout.LayoutParams( FrameLayout.LayoutParams.WrapContent, FrameLayout.LayoutParams.WrapContent, GravityFlags.Top | GravityFlags.CenterHorizontal) { TopMargin = 60 }); var cancel = new Button(this) { Text = "Annulla" }; cancel.Click += (_, _) => Finish(); root.AddView(cancel, new FrameLayout.LayoutParams( FrameLayout.LayoutParams.WrapContent, FrameLayout.LayoutParams.WrapContent, GravityFlags.Bottom | GravityFlags.CenterHorizontal) { BottomMargin = 80 }); SetContentView(root); _bgThread = new HandlerThread("CamBg"); _bgThread.Start(); _bgHandler = new Handler(_bgThread.Looper!); if (CheckSelfPermission(Android.Manifest.Permission.Camera) == Android.Content.PM.Permission.Granted) _permOk = true; else RequestPermissions([Android.Manifest.Permission.Camera], PermReq); } public override void OnRequestPermissionsResult(int req, string[] perms, Android.Content.PM.Permission[] grants) { _permOk = grants.Length > 0 && grants[0] == Android.Content.PM.Permission.Granted; if (_permOk) TryOpen(); else Finish(); } public void OnSurfaceTextureAvailable(SurfaceTexture st, int w, int h) { _surfaceTex = st; TryOpen(); } public bool OnSurfaceTextureDestroyed(SurfaceTexture st) { CloseCamera(); return true; } public void OnSurfaceTextureSizeChanged(SurfaceTexture st, int w, int h) { } public void OnSurfaceTextureUpdated(SurfaceTexture st) { } private void TryOpen() { if (_permOk && _surfaceTex != null) OpenCamera(_surfaceTex); } private void OpenCamera(SurfaceTexture st) { var mgr = (CameraManager)GetSystemService(CameraService)!; var ids = mgr.GetCameraIdList()!; string cameraId = ids[0]; foreach (var id in ids) { var chars = mgr.GetCameraCharacteristics(id)!; if (chars.Get(CameraCharacteristics.LensFacing) is Java.Lang.Integer f && f.IntValue() == (int)LensFacing.Back) { cameraId = id; break; } } var map = mgr.GetCameraCharacteristics(cameraId)! .Get(CameraCharacteristics.ScalerStreamConfigurationMap) as Android.Hardware.Camera2.Params.StreamConfigurationMap; var allSizes = map!.GetOutputSizes((int)ImageFormatType.Jpeg)!; var size = allSizes.OrderBy(s => Math.Abs(s.Width - 640)).First(); st.SetDefaultBufferSize(size.Width, size.Height); _imageReader = ImageReader.NewInstance(size.Width, size.Height, ImageFormatType.Jpeg, 2); _imageReader.SetOnImageAvailableListener(new ImageListener(this), _bgHandler); mgr.OpenCamera(cameraId, new OpenCb(this, st), _bgHandler); } private void CloseCamera() { _session?.Close(); _session = null; _camera?.Close(); _camera = null; _imageReader?.Close(); _imageReader = null; _bgThread?.QuitSafely(); } internal void OnCameraOpened(CameraDevice dev, SurfaceTexture st) { _camera = dev; var prevSurf = new Surface(st); var capSurf = _imageReader!.Surface!; #pragma warning disable CA1422 // CreateCaptureSession(IList,...) obsoleted on API 30; replacement needs API 28, our min is 23 dev.CreateCaptureSession([prevSurf, capSurf], new SessionCb(this, prevSurf, capSurf), _bgHandler); #pragma warning restore CA1422 } internal void OnSessionReady(CameraCaptureSession session, Surface prev, Surface cap) { _session = session; var req = _camera!.CreateCaptureRequest(CameraTemplate.Preview); req.AddTarget(prev); req.AddTarget(cap); session.SetRepeatingRequest(req.Build()!, null, _bgHandler); } internal void TryDecode(ImageReader reader) { if (_done) return; var image = reader.AcquireLatestImage(); if (image == null) return; try { var now = SystemClock.ElapsedRealtime(); if (now - _lastDecode < 400) return; // max ~2.5 fps _lastDecode = now; var buf = image.GetPlanes()![0].Buffer!; var bytes = new byte[buf.Remaining()]; buf.Get(bytes); var bmp = BitmapFactory.DecodeByteArray(bytes, 0, bytes.Length); if (bmp == null) return; int w = bmp.Width, h = bmp.Height; var pixels = new int[w * h]; bmp.GetPixels(pixels, 0, w, 0, 0, w, h); bmp.Recycle(); // ARGB int[] → RGB byte[] for ZXing.RGBLuminanceSource var rgb = new byte[pixels.Length * 3]; for (int i = 0; i < pixels.Length; i++) { rgb[i * 3] = (byte)(pixels[i] >> 16); rgb[i * 3 + 1] = (byte)(pixels[i] >> 8); rgb[i * 3 + 2] = (byte) pixels[i]; } var source = new RGBLuminanceSource(rgb, w, h, RGBLuminanceSource.BitmapFormat.RGB24); var zreader = new BarcodeReaderGeneric { AutoRotate = true }; zreader.Options.PossibleFormats = [BarcodeFormat.QR_CODE]; zreader.Options.TryHarder = true; var result = zreader.Decode(source); if (result == null) return; _done = true; RunOnUiThread(() => { var intent = new Intent().PutExtra(ResultKey, result.Text); SetResult(AndroidResult.Ok, intent); Finish(); }); } finally { image.Close(); } } protected override void OnDestroy() { CloseCamera(); base.OnDestroy(); } // ── Callback helpers ───────────────────────────────────────────────── private sealed class OpenCb(ScannerActivity a, SurfaceTexture st) : CameraDevice.StateCallback { public override void OnOpened(CameraDevice cam) => a.OnCameraOpened(cam, st); public override void OnDisconnected(CameraDevice cam) { cam.Close(); a.Finish(); } public override void OnError(CameraDevice cam, CameraError err) { cam.Close(); a.Finish(); } } private sealed class SessionCb(ScannerActivity a, Surface prev, Surface cap) : CameraCaptureSession.StateCallback { public override void OnConfigured(CameraCaptureSession s) => a.OnSessionReady(s, prev, cap); public override void OnConfigureFailed(CameraCaptureSession s) => a.Finish(); } private sealed class ImageListener(ScannerActivity a) : Java.Lang.Object, ImageReader.IOnImageAvailableListener { // Interface declares ImageReader? but reader is never null in practice public void OnImageAvailable(ImageReader? reader) => a.TryDecode(reader!); } }