Files
PalladiumWallet/src/App.Android/ScannerActivity.cs
T
davide 3d5a226a5a docs: translate all code comments to English (language policy)
Translate every Italian /// XML doc comment, <!-- --> XAML comment, and
// inline comment to English across all source files (Core, App, tests).
Add the language policy to CLAUDE.md (conversation Italian; all code
and docs English). Update one test assertion that checked an Italian
exception message that was also translated.
2026-06-16 14:40:11 +02:00

219 lines
8.6 KiB
C#

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;
/// <summary>
/// Full-screen activity for QR scanning via Camera2 + ZXing.Net.
/// Returns to MainActivity via SetResult with the extra "qr" = code text.
/// </summary>
[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!);
}
}