From 7d60a2012e85f88d9772a3e4041f5b3a12834ef4 Mon Sep 17 00:00:00 2001 From: "kasper@byolimit.com" Date: Wed, 31 May 2017 04:31:56 +0200 Subject: [PATCH] Fixes to ZXing[Texture|Surface]View after merge of PR #533 --- Samples/Forms/Core/CustomScanPage.cs | 2 +- Samples/Forms/Droid/FormsSample.Droid.csproj | 2 +- Samples/Forms/iOS/FormsSample.iOS.csproj | 8 - .../CameraAccess/CameraAnalyzer.cs | 70 +- .../CameraAccess/CameraController.cs | 72 +- .../CameraAccess/CameraEventsListener.cs | 33 +- .../CameraAccess/Torch.cs | 4 +- .../FastJavaArrayEx.cs | 21 +- .../ZXing.Net.Mobile.Android.csproj | 2 + .../ZXingSurfaceView.cs | 4 +- .../ZXingTextureView.cs | 1528 ++++++++--------- .../MobileBarcodeScanningOptions.cs | 9 +- .../ZXing.Net.Mobile.Forms.Android.csproj | 2 +- .../ZXingScannerViewRenderer.cs | 43 +- 14 files changed, 842 insertions(+), 958 deletions(-) diff --git a/Samples/Forms/Core/CustomScanPage.cs b/Samples/Forms/Core/CustomScanPage.cs index 2272b5838..2675d3363 100644 --- a/Samples/Forms/Core/CustomScanPage.cs +++ b/Samples/Forms/Core/CustomScanPage.cs @@ -28,7 +28,7 @@ public CustomScanPage () : base () formats.Add(ZXing.BarcodeFormat.CODE_39); formats.Add(ZXing.BarcodeFormat.QR_CODE); - zxing.Options.DelayBetweenContinuousScans = 1000; // same barcode can only be scanned once a second. Different barcodes is a different matter + zxing.Options.DelayBetweenContinuousScans = 1000; zxing.IsTorchOn = true; diff --git a/Samples/Forms/Droid/FormsSample.Droid.csproj b/Samples/Forms/Droid/FormsSample.Droid.csproj index 63cb797d0..811ad46d8 100644 --- a/Samples/Forms/Droid/FormsSample.Droid.csproj +++ b/Samples/Forms/Droid/FormsSample.Droid.csproj @@ -16,7 +16,7 @@ FormsSample.Droid Properties\AndroidManifest.xml v7.1 - v7.0 + v7.1 diff --git a/Samples/Forms/iOS/FormsSample.iOS.csproj b/Samples/Forms/iOS/FormsSample.iOS.csproj index fbe820f86..cb5306c9c 100644 --- a/Samples/Forms/iOS/FormsSample.iOS.csproj +++ b/Samples/Forms/iOS/FormsSample.iOS.csproj @@ -23,8 +23,6 @@ false i386 None - true - true true true iPhone Developer @@ -40,9 +38,7 @@ ARMv7, ARM64 Entitlements.plist true - true iPhone Developer - true full @@ -53,9 +49,7 @@ false i386 None - true iPhone Developer - true true @@ -72,8 +66,6 @@ iPhone Developer true true - true - true true diff --git a/Source/ZXing.Net.Mobile.Android/CameraAccess/CameraAnalyzer.cs b/Source/ZXing.Net.Mobile.Android/CameraAccess/CameraAnalyzer.cs index 7d5012344..b78fdfa82 100644 --- a/Source/ZXing.Net.Mobile.Android/CameraAccess/CameraAnalyzer.cs +++ b/Source/ZXing.Net.Mobile.Android/CameraAccess/CameraAnalyzer.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Android.Views; using ApxLabs.FastAndroidCamera; namespace ZXing.Mobile.CameraAccess @@ -10,18 +9,16 @@ public class CameraAnalyzer { private readonly CameraController _cameraController; private readonly MobileBarcodeScanningOptions _scanningOptions; - private readonly CameraEventsListener _cameraEventListener; - private Task _processingTask; private DateTime _lastPreviewAnalysis = DateTime.UtcNow; private bool _wasScanned; private BarcodeReaderGeneric _barcodeReader; - public CameraAnalyzer(SurfaceView surfaceView, MobileBarcodeScanningOptions scanningOptions) + public CameraAnalyzer(CameraController cameraController, MobileBarcodeScanningOptions scanningOptions) { _scanningOptions = scanningOptions; - _cameraEventListener = new CameraEventsListener(); - _cameraController = new CameraController(surfaceView, _cameraEventListener, scanningOptions); - Torch = new Torch(_cameraController, surfaceView.Context); + _cameraController = cameraController; + + Torch = new Torch(_cameraController); } public event EventHandler BarcodeFound; @@ -43,13 +40,13 @@ public void ResumeAnalysis() public void ShutdownCamera() { IsAnalyzing = false; - _cameraEventListener.OnPreviewFrameReady -= HandleOnPreviewFrameReady; + _cameraController.OnPreviewFrameReady -= HandleOnPreviewFrameReady; _cameraController.ShutdownCamera(); } public void SetupCamera() { - _cameraEventListener.OnPreviewFrameReady += HandleOnPreviewFrameReady; + _cameraController.OnPreviewFrameReady += HandleOnPreviewFrameReady; _cameraController.SetupCamera(); } @@ -75,11 +72,6 @@ private bool CanAnalyzeFrame if (!IsAnalyzing) return false; - //Check and see if we're still processing a previous frame - // todo: check if we can run as many as possible or mby run two analyzers at once (Vision + ZXing) - if (_processingTask != null && !_processingTask.IsCompleted) - return false; - var elapsedTimeMs = (DateTime.UtcNow - _lastPreviewAnalysis).TotalMilliseconds; if (elapsedTimeMs < _scanningOptions.DelayBetweenAnalyzingFrames) return false; @@ -100,19 +92,14 @@ private void HandleOnPreviewFrameReady(object sender, FastJavaByteArray fastArra _wasScanned = false; _lastPreviewAnalysis = DateTime.UtcNow; - _processingTask = Task.Run(() => - { - try - { - DecodeFrame(fastArray); - } catch (Exception ex) { - Console.WriteLine(ex); - } - }).ContinueWith(task => + try + { + DecodeFrame(fastArray); + } + catch (Exception ex) { - if (task.IsFaulted) - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "DecodeFrame exception occurs"); - }, TaskContinuationOptions.OnlyOnFaulted); + Android.Util.Log.Debug(MobileBarcodeScanner.TAG, $"DecodeFrame exception occured: {ex.Message}"); + } } private byte[] buffer; @@ -131,28 +118,29 @@ private void DecodeFrame(FastJavaByteArray fastArray) Result result = null; var start = PerformanceCounter.Start(); - if (rotate) - fastArray.RotateInPlace(ref buffer, width, height); + if (rotate) + { + fastArray.Transpose(ref buffer, width, height); + var tmp = width; + width = height; + height = tmp; + } var luminanceSource = new FastJavaByteArrayYUVLuminanceSource(fastArray, width, height, 0, 0, width, height); // _area.Left, _area.Top, _area.Width, _area.Height); result = _barcodeReader.Decode(luminanceSource); - fastArray.Dispose(); - fastArray = null; - - PerformanceCounter.Stop(start, - "Decode Time: {0} ms (width: " + width + ", height: " + height + ", degrees: " + cDegrees + ", rotate: " + - rotate + ")"); + PerformanceCounter.Stop(start, "Decode Time: {0} ms (width: " + width + ", height: " + height + ", degrees: " + cDegrees + ", rotate: " + rotate + ")"); - if (result != null) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Barcode Found: " + result.Text); + if (result != null) + { + Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Barcode Found: " + result.Text); - _wasScanned = true; - BarcodeFound?.Invoke(this, result); - return; - } + _wasScanned = true; + BarcodeFound?.Invoke(this, result); + } + else + AutoFocus(); } private void InitBarcodeReaderIfNeeded() diff --git a/Source/ZXing.Net.Mobile.Android/CameraAccess/CameraController.cs b/Source/ZXing.Net.Mobile.Android/CameraAccess/CameraController.cs index 543d00833..1520864d0 100644 --- a/Source/ZXing.Net.Mobile.Android/CameraAccess/CameraController.cs +++ b/Source/ZXing.Net.Mobile.Android/CameraAccess/CameraController.cs @@ -17,22 +17,35 @@ public class CameraController private readonly Context _context; private readonly MobileBarcodeScanningOptions _scanningOptions; private readonly ISurfaceHolder _holder; - private readonly SurfaceView _surfaceView; private readonly CameraEventsListener _cameraEventListener; private int _cameraId; + private bool _autoFocusCycleDone = true; + private bool _useContinousFocus; public CameraController(SurfaceView surfaceView, CameraEventsListener cameraEventListener, MobileBarcodeScanningOptions scanningOptions) { + SurfaceView = surfaceView; + _context = surfaceView.Context; + _scanningOptions = scanningOptions; _holder = surfaceView.Holder; - _surfaceView = surfaceView; + _cameraEventListener = cameraEventListener; - _scanningOptions = scanningOptions; + _cameraEventListener.AutoFocus += (s, e) => + _autoFocusCycleDone = true; } + public SurfaceView SurfaceView { get; } + public Camera Camera { get; private set; } + public event EventHandler OnPreviewFrameReady + { + add { _cameraEventListener.OnPreviewFrameReady += value; } + remove { _cameraEventListener.OnPreviewFrameReady -= value; } + } + public int LastCameraDisplayOrientationDegree { get; private set; } public void RefreshCamera() @@ -78,13 +91,8 @@ public void SetupCamera() int bufferSize = (previewSize.Width * previewSize.Height * bitsPerPixel) / 8; - const int NUM_PREVIEW_BUFFERS = 5; - for (uint i = 0; i < NUM_PREVIEW_BUFFERS; ++i) - { - using (var buffer = new FastJavaByteArray(bufferSize)) - Camera.AddCallbackBuffer(buffer); - } - + using (var buffer = new FastJavaByteArray(bufferSize)) + Camera.AddCallbackBuffer(buffer); Camera.StartPreview(); @@ -117,8 +125,8 @@ public void AutoFocus(int x, int y) { // The bounds for focus areas are actually -1000 to 1000 // So we need to translate the touch coordinates to this scale - var focusX = x / _surfaceView.Width * 2000 - 1000; - var focusY = y / _surfaceView.Height * 2000 - 1000; + var focusX = x / SurfaceView.Width * 2000 - 1000; + var focusY = y / SurfaceView.Height * 2000 - 1000; // Call the autofocus with our coords AutoFocus(focusX, focusY, true); @@ -134,10 +142,9 @@ public void ShutdownCamera() { try { - //Camera.SetPreviewCallback(null); Camera.SetPreviewDisplay(null); Camera.StopPreview(); - Camera.SetNonMarshalingPreviewCallback(null); + Camera.SetNonMarshalingPreviewCallback(null); // replaces Camera.SetPreviewCallback(null); } catch (Exception ex) { @@ -201,11 +208,6 @@ private void OpenCamera() { Camera = Camera.Open(); } - - //if (Camera != null) - // Camera.SetPreviewCallback(_cameraEventListener); - //else - // MobileBarcodeScanner.LogWarn(MobileBarcodeScanner.TAG, "Camera is null :("); } catch (Exception ex) { @@ -217,8 +219,13 @@ private void OpenCamera() private void ApplyCameraSettings() { var parameters = Camera.GetParameters(); - parameters.PreviewFormat = ImageFormatType.Nv21; + parameters.PreviewFormat = ImageFormatType.Nv21; // YCrCb format (all Android devices must support this) + + // Android actually defines a barcode scene mode .. + if (parameters.SupportedSceneModes.Contains(Camera.Parameters.SceneModeBarcode)) // .. we might be lucky :-) + parameters.SceneMode = Camera.Parameters.SceneModeBarcode; + // First try continuous video, then auto focus, then fixed var supportedFocusModes = parameters.SupportedFocusModes; if (Build.VERSION.SdkInt >= BuildVersionCodes.IceCreamSandwich && supportedFocusModes.Contains(Camera.Parameters.FocusModeContinuousPicture)) @@ -230,20 +237,6 @@ private void ApplyCameraSettings() else if (supportedFocusModes.Contains(Camera.Parameters.FocusModeFixed)) parameters.FocusMode = Camera.Parameters.FocusModeFixed; - var selectedFps = parameters.SupportedPreviewFpsRange.FirstOrDefault(); - if (selectedFps != null) - { - // This will make sure we select a range with the lowest minimum FPS - // and maximum FPS which still has the lowest minimum - // This should help maximize performance / support for hardware - foreach (var fpsRange in parameters.SupportedPreviewFpsRange) - { - if (fpsRange[0] <= selectedFps[0] && fpsRange[1] > selectedFps[1]) - selectedFps = fpsRange; - } - parameters.SetPreviewFpsRange(selectedFps[0], selectedFps[1]); - } - var availableResolutions = parameters.SupportedPreviewSizes.Select(sps => new CameraResolution { Width = sps.Width, @@ -292,12 +285,18 @@ private void ApplyCameraSettings() Camera.SetParameters(parameters); + parameters = Camera.GetParameters(); // refresh to see what is actually set! + + _useContinousFocus = parameters.FocusMode == Camera.Parameters.FocusModeContinuousPicture || parameters.FocusMode == Camera.Parameters.FocusModeContinuousVideo; + SetCameraDisplayOrientation(); } private void AutoFocus(int x, int y, bool useCoordinates) { - if (Camera == null) return; + if (_useContinousFocus || !_autoFocusCycleDone || Camera == null) + return; + var cameraParams = Camera.GetParameters(); Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Requested"); @@ -317,7 +316,7 @@ private void AutoFocus(int x, int y, bool useCoordinates) // So we'll offset -10 from the center of the touch and then // make a rect of 20 to give an area to focus on based on the center of the touch x = x - 10; - y = y - 10; + y = y - 10; // todo: ensure positive! // Ensure we don't go over the -1000 to 1000 limit of focus area if (x >= 1000) @@ -340,6 +339,7 @@ private void AutoFocus(int x, int y, bool useCoordinates) } // Finally autofocus (weather we used focus areas or not) + _autoFocusCycleDone = false; Camera.AutoFocus(_cameraEventListener); } catch (Exception ex) diff --git a/Source/ZXing.Net.Mobile.Android/CameraAccess/CameraEventsListener.cs b/Source/ZXing.Net.Mobile.Android/CameraAccess/CameraEventsListener.cs index fcf0c224f..9b8ceaea0 100644 --- a/Source/ZXing.Net.Mobile.Android/CameraAccess/CameraEventsListener.cs +++ b/Source/ZXing.Net.Mobile.Android/CameraAccess/CameraEventsListener.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Android.Hardware; using ApxLabs.FastAndroidCamera; @@ -6,26 +7,30 @@ namespace ZXing.Mobile.CameraAccess { public class CameraEventsListener : Java.Lang.Object, INonMarshalingPreviewCallback, Camera.IAutoFocusCallback { - public event EventHandler OnPreviewFrameReady; + public event EventHandler OnPreviewFrameReady; + public event EventHandler AutoFocus; - //public void OnPreviewFrame(byte[] data, Camera camera) - //{ - // OnPreviewFrameReady?.Invoke(this, data); - //} - - public void OnPreviewFrame(IntPtr data, Camera camera) +#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void + public async void OnPreviewFrame(IntPtr data, Camera camera) { - using (var fastArray = new FastJavaByteArray(data)) - { - OnPreviewFrameReady?.Invoke(this, fastArray); - - camera.AddCallbackBuffer(fastArray); - } + try + { + using (var fastArray = new FastJavaByteArray(data)) + { + await Task.Run(() => OnPreviewFrameReady?.Invoke(this, fastArray)); + camera.AddCallbackBuffer(fastArray); + } + } + catch (Exception ex) + { + Android.Util.Log.Warn(MobileBarcodeScanner.TAG, $"Exception squashed! {ex.Message}"); + } } +#pragma warning restore RECS0165 // Asynchronous methods should return a Task instead of void public void OnAutoFocus(bool success, Camera camera) { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus {0}", success ? "Succeeded" : "Failed"); + AutoFocus?.Invoke(this, success); } } } \ No newline at end of file diff --git a/Source/ZXing.Net.Mobile.Android/CameraAccess/Torch.cs b/Source/ZXing.Net.Mobile.Android/CameraAccess/Torch.cs index 2ec8c9c5d..eeda39c02 100644 --- a/Source/ZXing.Net.Mobile.Android/CameraAccess/Torch.cs +++ b/Source/ZXing.Net.Mobile.Android/CameraAccess/Torch.cs @@ -10,10 +10,10 @@ public class Torch private readonly Context _context; private bool? _hasTorch; - public Torch(CameraController cameraController, Context context) + public Torch(CameraController cameraController) { _cameraController = cameraController; - _context = context; + _context = cameraController.SurfaceView.Context; } public bool IsSupported diff --git a/Source/ZXing.Net.Mobile.Android/FastJavaArrayEx.cs b/Source/ZXing.Net.Mobile.Android/FastJavaArrayEx.cs index 0bb8d068b..ed877fa08 100644 --- a/Source/ZXing.Net.Mobile.Android/FastJavaArrayEx.cs +++ b/Source/ZXing.Net.Mobile.Android/FastJavaArrayEx.cs @@ -16,30 +16,37 @@ public static void BlockCopyTo(this FastJavaByteArray self, int sourceIndex, byt } static readonly ThreadLocal _buffer = new ThreadLocal(); - public static void RotateInPlace(this FastJavaByteArray self, int width, int height) + public static void Transpose(this FastJavaByteArray self, int width, int height) { var data = _buffer.Value; - self.RotateInPlace(ref data, width, height); + self.Transpose(ref data, width, height); _buffer.Value = data; } - public static void RotateInPlace(this FastJavaByteArray self, ref byte[] buffer, int width, int height) + public static void Transpose(this FastJavaByteArray self, ref byte[] buffer, int width, int height) { var length = self.Count; if (length < width * height) throw new ArgumentException($"(this.Count) {length} < {width * height} = {width} * {height} (width * height)"); + // todo: Make transpose in-place, but this is not trivial for a non-square matrix, encoded in a 1d array. + // Currently we spend a bit of time + if (buffer == null || buffer.Length < length) buffer = new byte[length]; // ensure we have enough buffer space for the operation - self.BlockCopyTo(0, buffer, 0, length); + self.BlockCopyTo(0, buffer, 0, length); // this is fairly quick (~1ms per MiB) unsafe { - for (var y = 0; y < height; y++) - for (var x = 0; x < width; x++) - self.Raw[x * height + height - y - 1] = buffer[x + y * width]; + // This loop is kind of slow (~20ms per MiB) + fixed (byte* src = &buffer[0]) + { + for (var y = 0; y < height; y++) + for (var x = 0; x < width; x++) + self.Raw[y + x * height] = src[x + y * width]; + } } } } diff --git a/Source/ZXing.Net.Mobile.Android/ZXing.Net.Mobile.Android.csproj b/Source/ZXing.Net.Mobile.Android/ZXing.Net.Mobile.Android.csproj index d4d911c81..eb1d6374d 100644 --- a/Source/ZXing.Net.Mobile.Android/ZXing.Net.Mobile.Android.csproj +++ b/Source/ZXing.Net.Mobile.Android/ZXing.Net.Mobile.Android.csproj @@ -45,6 +45,8 @@ ..\..\packages\FastAndroidCamera.2.0.0\lib\MonoAndroid403\FastAndroidCamera.dll + + diff --git a/Source/ZXing.Net.Mobile.Android/ZXingSurfaceView.cs b/Source/ZXing.Net.Mobile.Android/ZXingSurfaceView.cs index 777d6c976..9b410451d 100644 --- a/Source/ZXing.Net.Mobile.Android/ZXingSurfaceView.cs +++ b/Source/ZXing.Net.Mobile.Android/ZXingSurfaceView.cs @@ -4,6 +4,7 @@ using Android.Views; using Android.Graphics; using ZXing.Mobile.CameraAccess; +using Android.OS; namespace ZXing.Mobile { @@ -24,9 +25,8 @@ protected ZXingSurfaceView(IntPtr javaReference, JniHandleOwnership transfer) private void Init() { - _cameraAnalyzer = new CameraAnalyzer(this, ScanningOptions); + _cameraAnalyzer = new CameraAnalyzer(new CameraController(this, new CameraEventsListener(), ScanningOptions), ScanningOptions); Holder.AddCallback(this); - Holder.SetType(SurfaceType.PushBuffers); } public async void SurfaceCreated(ISurfaceHolder holder) diff --git a/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs b/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs index e1dcd93a4..437b95337 100644 --- a/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs +++ b/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs @@ -9,839 +9,729 @@ using Android.Content.PM; using Android.Graphics; using Android.Hardware; -using Android.Opengl; using Android.OS; using Android.Runtime; using Android.Util; using Android.Views; using Android.Widget; - using ApxLabs.FastAndroidCamera; - -using Javax.Microedition.Khronos.Egl; - +using ZXing.Net.Mobile.Android; using Camera = Android.Hardware.Camera; using Matrix = Android.Graphics.Matrix; namespace ZXing.Mobile { - public static class IntEx - { - public static bool Between(this int i, int lower, int upper) - { - return lower <= i && i <= upper; - } - } - - public static class HandlerEx - { - public static void PostSafe(this Handler self, Action action) - { - self.Post(() => - { - try - { - action(); - } - catch (Exception ex) - { - // certain death, unless we squash - Log.Debug(MobileBarcodeScanner.TAG, $"Squashing: {ex} to avoid certain death! Handler is: {self.GetHashCode()}"); - } - }); - } - - public static void PostSafe(this Handler self, Func action) - { - self.Post(async () => - { - try - { - await action(); - } - catch (Exception ex) - { - // certain death, unless we squash - Log.Debug(MobileBarcodeScanner.TAG, $"Squashing: {ex} to avoid certain death! Handler is: {self.GetHashCode()}"); - } - }); - } - - } - - public static class RectFEx { - public static void Flip(this RectF s) { - var tmp = s.Left; - s.Left = s.Top; - s.Top = tmp; - tmp = s.Right; - s.Right = s.Bottom; - s.Bottom = tmp; - } - } - - class MyOrientationEventListener : OrientationEventListener - { - public MyOrientationEventListener(Context context, SensorDelay delay) : base(context, delay) { } - - public event Action OrientationChanged; - - public override void OnOrientationChanged(int orientation) - { - OrientationChanged?.Invoke(orientation); - } - } - - public class RingBuffer - { - readonly T[] _buffer; - int _tail; - int _length; - - public RingBuffer(int capacity) - { - _buffer = new T[capacity]; - } - - public void Add(T item) - { - _buffer[_tail] = item; // will overwrite existing entry, if any - _tail = (_tail + 1) % _buffer.Length; // roll over - _length++; - } - - public T this[int index] - { - get { return _buffer[WrapIndex(index)]; } - set { _buffer[WrapIndex(index)] = value; } - } - - public int Length - { - get { return _length; } - } - - public int FindIndex(ref T toFind, IComparer comparer = null) - { - comparer = comparer ?? Comparer.Default; - int idx = -1; - for (int i = 0; i < Length; ++i) - { - var candidate = this[i]; - if (comparer.Compare(candidate, toFind) == 0) - { - idx = i; - toFind = candidate; - break; // item found in history ring - } - } - return idx; - } - - public void AddOrUpdate(ref T item, IComparer comparer = null) - { - var idx = FindIndex(ref item); - if (idx < 0) - Add(item); - else - this[idx] = item; - } - - int Head - { - get { return (_tail - _length) % _buffer.Length; } - } - - int WrapIndex(int index) - { - if (index < 0 || index >= _length) - throw new IndexOutOfRangeException($"{nameof(index)} = {index}"); - - return (Head + index) % _buffer.Length; - } - } - - public class ZXingTextureView : TextureView, IScannerView, Camera.IAutoFocusCallback, INonMarshalingPreviewCallback - { - Camera.CameraInfo _cameraInfo; - Camera _camera; - - static ZXingTextureView() { - } - - public ZXingTextureView(IntPtr javaRef, JniHandleOwnership transfer) : base(javaRef, transfer) - { - Init(); - } - - public ZXingTextureView(Context ctx) : base(ctx) - { - Init(); - } - - public ZXingTextureView(Context ctx, IAttributeSet attr) : base(ctx, attr) - { - Init(); - } - - public ZXingTextureView(Context ctx, IAttributeSet attr, int defStyle) : base(ctx, attr, defStyle) - { - Init(); - } - - Toast _toast; - Handler _handler; - MyOrientationEventListener _orientationEventListener; - TaskCompletionSource _surfaceAvailable = new TaskCompletionSource(); - SurfaceTexture _surfaceTexture; - void Init() - { - _toast = Toast.MakeText(Context, string.Empty, ToastLength.Short); - - var handlerThread = new HandlerThread("ZXingTextureView"); - handlerThread.Start(); - _handler = new Handler(handlerThread.Looper); - - // We have to handle changes to screen orientation explicitly, as we cannot rely on OnConfigurationChanges - _orientationEventListener = new MyOrientationEventListener(Context, SensorDelay.Normal); - _orientationEventListener.OrientationChanged += OnOrientationChanged; - if (_orientationEventListener.CanDetectOrientation()) - _orientationEventListener.Enable(); - - SurfaceTextureAvailable += (sender, e) => - _surfaceAvailable.SetResult(e); - - SurfaceTextureSizeChanged += (sender, e) => - SetSurfaceTransform(e.Surface, e.Width, e.Height); - - SurfaceTextureDestroyed += (sender, e) => - { - ShutdownCamera(); - _surfaceAvailable = new TaskCompletionSource(); - _surfaceTexture = null; - }; - } - - Camera.Size PreviewSize { get; set; } - - int _lastOrientation; - SurfaceOrientation _lastSurfaceOrientation; - void OnOrientationChanged(int orientation) - { - // - // This code should only run when UI snaps into either portrait or landscape mode. - // At first glance we could just override OnConfigurationChanged, but unfortunately - // a rotation from landscape directly to reverse landscape won't fire an event - // (which is easily done by rotating via upside-down on many devices), because Android - // can just reuse the existing config and handle the rotation automatically .. - // - // .. except of course for camera orientation, which must handled explicitly *sigh*. - // Hurray Google, you sure suck at API design! - // - // Instead we waste some CPU by tracking orientation down to the last degree, every 200ms. - // I have yet to come up with a better way. - // - if (_camera == null) - return; - - var o = (((orientation + 45) % 360) / 90) * 90; // snap to 0, 90, 180, or 270. - if (o == _lastOrientation) - return; // fast path, no change .. - - // Actual snap is delayed, so check if we are actually rotated - var rotation = WindowManager.DefaultDisplay.Rotation; - if (rotation == _lastSurfaceOrientation) - return; // .. still no change - - _lastOrientation = o; - _lastSurfaceOrientation = rotation; - - _handler.PostSafe(() => - { - _camera?.SetDisplayOrientation(CameraOrientation(WindowManager.DefaultDisplay.Rotation)); // and finally, the interesting part *sigh* - }); - } - - bool IsPortrait { - get { - var rotation = WindowManager.DefaultDisplay.Rotation; - return rotation == SurfaceOrientation.Rotation0 || rotation == SurfaceOrientation.Rotation180; - } - } - - Rectangle _area; - void SetSurfaceTransform(SurfaceTexture st, int width, int height) - { - var p = PreviewSize; - if (p == null) - return; // camera no ready yet, we will be called again later from SetupCamera. - - using (var metrics = new DisplayMetrics()) - { - #region transform - // Compensate for non-square pixels - WindowManager.DefaultDisplay.GetMetrics(metrics); - var aspectRatio = metrics.Xdpi / metrics.Ydpi; // close to 1, but rarely perfect 1 - - // Compensate for preview streams aspect ratio - aspectRatio *= (float)p.Height / p.Width; - - // Compensate for portrait mode - if (IsPortrait) - aspectRatio = 1f / aspectRatio; - - // OpenGL coordinate system goes form 0 to 1 - var transform = new Matrix(); - transform.SetScale(1f, aspectRatio * width / height); // lock on to width - - Post(() => { - try - { - SetTransform(transform); - } - catch (ObjectDisposedException) { } // todo: What to do here?! For now we squash :-/ - }); // ensure we use the right thread when updating transform - - Log.Debug(MobileBarcodeScanner.TAG, $"Aspect ratio: {aspectRatio}, Transform: {transform}"); - - #endregion - - #region area - using (var max = new RectF(0, 0, p.Width, p.Height)) - using (var r = new RectF(max)) - { - // Calculate area of interest within preview - var inverse = new Matrix(); - transform.Invert(inverse); - - Log.Debug(MobileBarcodeScanner.TAG, $"Inverse: {inverse}"); - - var flip = IsPortrait; - if (flip) r.Flip(); - inverse.MapRect(r); - if (flip) r.Flip(); - - r.Intersect(max); // stream doesn't always fill the view! - - // Compensate for reverse mounted camera, like on the Nexus 5X. - var reverse = _cameraInfo.Orientation == 270; - if (reverse) - { - if (flip) - r.OffsetTo(p.Width - r.Right, 0); // shift area right - else - r.Offset(0, p.Height - r.Bottom); // shift are down - } - - _area = new Rectangle((int)r.Left, (int)r.Top, (int)r.Width(), (int)r.Height()); - - Log.Debug(MobileBarcodeScanner.TAG, $"Area: {_area}"); - } - #endregion - } - } - - IWindowManager _wm; - IWindowManager WindowManager - { - get - { - _wm = _wm ?? Context.GetSystemService(Context.WindowService).JavaCast(); - return _wm; - } - } - - bool? _hasTorch; - public bool HasTorch - { - get - { - if (_hasTorch.HasValue) - return _hasTorch.Value; - - var p = _camera.GetParameters(); - var supportedFlashModes = p.SupportedFlashModes; - - if (supportedFlashModes != null - && (supportedFlashModes.Contains(Camera.Parameters.FlashModeTorch) - || supportedFlashModes.Contains(Camera.Parameters.FlashModeOn))) - _hasTorch = CheckTorchPermissions(false); - - return _hasTorch.HasValue && _hasTorch.Value; - } - } - - bool _isAnalyzing; - public bool IsAnalyzing - { - get { return _isAnalyzing; } - } - - bool _isTorchOn; - public bool IsTorchOn - { - get { return _isTorchOn; } - } - - MobileBarcodeScanningOptions _scanningOptions; - IBarcodeReaderGeneric _barcodeReader; - public MobileBarcodeScanningOptions ScanningOptions - { - get { return _scanningOptions; } - set - { - _scanningOptions = value; - _delay = TimeSpan.FromMilliseconds(value.DelayBetweenContinuousScans).Ticks; - _barcodeReader = CreateBarcodeReader(value); - } - } - - bool _useContinuousFocus; - bool _autoFocusRunning; - public void AutoFocus() - { - _handler.PostSafe(() => - { - var camera = _camera; - if (camera == null || _autoFocusRunning || _useContinuousFocus) - return; // Allow camera to complete autofocus cycle, before trying again! - - _autoFocusRunning = true; - camera.AutoFocus(this); - }); - } - - public void AutoFocus(int x, int y) - { - // todo: Needs some slightly serious math to map back to camera coordinates. - // The method used in ZXingSurfaceView is simply wrong. - AutoFocus(); - } - - public void OnAutoFocus(bool focus, Camera camera) - { - _autoFocusRunning = false; - if (!(focus || _useContinuousFocus)) - AutoFocus(); - } - - - public void PauseAnalysis() - { - _isAnalyzing = false; - } - - public void ResumeAnalysis() - { - _isAnalyzing = true; - } - - Action _callback; - public void StartScanning(Action scanResultCallback, MobileBarcodeScanningOptions options = null) - { - _callback = scanResultCallback; - ScanningOptions = options ?? MobileBarcodeScanningOptions.Default; - - _handler.PostSafe(SetupCamera); - - ResumeAnalysis(); - } - - void OpenCamera() - { - if (_camera != null) - return; - - CheckCameraPermissions(); - - if (Build.VERSION.SdkInt >= BuildVersionCodes.Gingerbread) // Choose among multiple cameras from Gingerbread forward - { - int max = Camera.NumberOfCameras; - Log.Debug(MobileBarcodeScanner.TAG, $"Found {max} cameras"); - var requestedFacing = CameraFacing.Back; // default to back facing camera, .. - if (ScanningOptions.UseFrontCameraIfAvailable.HasValue && ScanningOptions.UseFrontCameraIfAvailable.Value) - requestedFacing = CameraFacing.Front; // .. but use front facing if available and requested - - var info = new Camera.CameraInfo(); - int idx = 0; - do - { - Camera.GetCameraInfo(idx++, info); // once again Android sucks! - } - while (info.Facing != requestedFacing && idx < max); - --idx; - - Log.Debug(MobileBarcodeScanner.TAG, $"Opening {info.Facing} facing camera: {idx}..."); - _cameraInfo = info; - _camera = Camera.Open(idx); - } - else { - _camera = Camera.Open(); - } - - _camera.Lock(); - } - - async Task SetupCamera() - { - OpenCamera(); - - var p = _camera.GetParameters(); - p.PreviewFormat = ImageFormatType.Nv21; // YCrCb format (all Android devices must support this) - - // First try continuous video, then auto focus, then fixed - var supportedFocusModes = p.SupportedFocusModes; - if (supportedFocusModes.Contains(Camera.Parameters.FocusModeContinuousVideo)) - p.FocusMode = Camera.Parameters.FocusModeContinuousVideo; - else if (supportedFocusModes.Contains(Camera.Parameters.FocusModeAuto)) - p.FocusMode = Camera.Parameters.FocusModeAuto; - else if (supportedFocusModes.Contains(Camera.Parameters.FocusModeFixed)) - p.FocusMode = Camera.Parameters.FocusModeFixed; - - // Check if we can support requested resolution .. - var availableResolutions = p.SupportedPreviewSizes.Select(s => new CameraResolution { Width = s.Width, Height = s.Height }).ToList(); - var resolution = ScanningOptions.GetResolution(availableResolutions); - - // .. If not, let's try and find a suitable one - resolution = resolution ?? availableResolutions.OrderBy(r => r.Width).FirstOrDefault(r => r.Width.Between(640, 1280) && r.Height.Between(640, 960)); - - // Hopefully a resolution was selected at some point - if (resolution != null) - p.SetPreviewSize(resolution.Width, resolution.Height); - - _camera.SetParameters(p); - - SetupTorch(_isTorchOn); - - p = _camera.GetParameters(); // refresh! - - _useContinuousFocus = p.FocusMode == Camera.Parameters.FocusModeContinuousVideo; - PreviewSize = p.PreviewSize; // get actual preview size (may differ from requested size) - var bitsPerPixel = ImageFormat.GetBitsPerPixel(p.PreviewFormat); - - Log.Debug(MobileBarcodeScanner.TAG, $"Preview size {PreviewSize.Width}x{PreviewSize.Height} with {bitsPerPixel} bits per pixel"); - - var surfaceInfo = await _surfaceAvailable.Task; - _surfaceTexture = surfaceInfo.Surface; - - SetSurfaceTransform(surfaceInfo.Surface, surfaceInfo.Width, surfaceInfo.Height); - - _camera.SetDisplayOrientation(CameraOrientation(WindowManager.DefaultDisplay.Rotation)); - _camera.SetPreviewTexture(surfaceInfo.Surface); - _camera.StartPreview(); - - int bufferSize = (PreviewSize.Width * PreviewSize.Height * bitsPerPixel) / 8; - using (var buffer = new FastJavaByteArray(bufferSize)) - _camera.AddCallbackBuffer(buffer); - - _camera.SetNonMarshalingPreviewCallback(this); - - // Docs suggest if Auto or Macro modes, we should invoke AutoFocus at least once - _autoFocusRunning = false; - if (!_useContinuousFocus) - AutoFocus(); - } - - public int CameraOrientation(SurfaceOrientation rotation) - { - int degrees = 0; - switch (rotation) - { - case SurfaceOrientation.Rotation0: - degrees = 0; - break; - case SurfaceOrientation.Rotation90: - degrees = 90; - break; - case SurfaceOrientation.Rotation180: - degrees = 180; - break; - case SurfaceOrientation.Rotation270: - degrees = 270; - break; - } - - // Handle front facing camera - if (_cameraInfo.Facing == CameraFacing.Front) - return (360 - ((_cameraInfo.Orientation + degrees) % 360)) % 360; // compensate for mirror - - return (_cameraInfo.Orientation - degrees + 360) % 360; - } - - void ShutdownCamera() - { - _handler.Post(() => - { - if (_camera == null) - return; - - var camera = _camera; - _camera = null; - - try - { - camera.StopPreview(); - camera.SetNonMarshalingPreviewCallback(null); - ClearSurface(_surfaceTexture); - } - catch (Exception e) - { - Log.Error(MobileBarcodeScanner.TAG, e.ToString()); - } - finally - { - camera.Release(); - } - }); - } - - void ClearSurface(SurfaceTexture texture) - { - if (texture == null) - return; - - var egl = (IEGL10)EGLContext.EGL; - var display = egl.EglGetDisplay(EGL10.EglDefaultDisplay); - egl.EglInitialize(display, null); - - int[] attribList = { - EGL10.EglRedSize, 8, - EGL10.EglGreenSize, 8, - EGL10.EglBlueSize, 8, - EGL10.EglAlphaSize, 8, - EGL10.EglRenderableType, EGL10.EglWindowBit, - EGL10.EglNone, 0, // placeholder for recordable [@-3] - EGL10.EglNone - }; - - var configs = new EGLConfig[1]; - int[] numConfigs = new int[1]; - egl.EglChooseConfig(display, attribList, configs, configs.Length, numConfigs); - var config = configs[0]; - var context = egl.EglCreateContext(display, config, EGL10.EglNoContext, new int[] { 12440, 2, EGL10.EglNone }); - - var eglSurface = egl.EglCreateWindowSurface(display, config, texture, new int[] { EGL10.EglNone }); - - egl.EglMakeCurrent(display, eglSurface, eglSurface, context); - GLES20.GlClearColor(0, 0, 0, 1); // black, no opacity - GLES20.GlClear(GLES20.GlColorBufferBit); - egl.EglSwapBuffers(display, eglSurface); - egl.EglDestroySurface(display, eglSurface); - egl.EglMakeCurrent(display, EGL10.EglNoSurface, EGL10.EglNoSurface, EGL10.EglNoContext); - egl.EglDestroyContext(display, context); - egl.EglTerminate(display); - } - - public void StopScanning() - { - PauseAnalysis(); - ShutdownCamera(); - } - - public void Torch(bool on) - { - if (!Context.PackageManager.HasSystemFeature(PackageManager.FeatureCameraFlash)) - { - Log.Info(MobileBarcodeScanner.TAG, "Flash not supported on this device"); - return; - } - - CheckTorchPermissions(); - - _isTorchOn = on; - if (_camera != null) // already running - SetupTorch(on); - } - - public void ToggleTorch() - { - Torch(!_isTorchOn); - } - - void SetupTorch(bool on) - { - var p = _camera.GetParameters(); - var supportedFlashModes = p.SupportedFlashModes ?? Enumerable.Empty(); - - string flashMode = null; - - if (on) - { - if (supportedFlashModes.Contains(Camera.Parameters.FlashModeTorch)) - flashMode = Camera.Parameters.FlashModeTorch; - else if (supportedFlashModes.Contains(Camera.Parameters.FlashModeOn)) - flashMode = Camera.Parameters.FlashModeOn; - } - else - { - if (supportedFlashModes.Contains(Camera.Parameters.FlashModeOff)) - flashMode = Camera.Parameters.FlashModeOff; - } - - if (!string.IsNullOrEmpty(flashMode)) - { - p.FlashMode = flashMode; - _camera.SetParameters(p); - } - } - - bool CheckCameraPermissions(bool throwOnError = true) - { - return CheckPermissions(Android.Manifest.Permission.Camera, throwOnError); - } - - bool CheckTorchPermissions(bool throwOnError = true) - { - return CheckPermissions(Android.Manifest.Permission.Flashlight, throwOnError); - } - - bool CheckPermissions(string permission, bool throwOnError = true) - { - Log.Debug(MobileBarcodeScanner.TAG, $"Checking {permission}..."); - - if (!PlatformChecks.IsPermissionInManifest(Context, permission) - || !PlatformChecks.IsPermissionGranted(Context, permission)) - { - var msg = $"Requires: {permission}, but was not found in your AndroidManifest.xml file."; - Log.Error(MobileBarcodeScanner.TAG, msg); - - if (throwOnError) - throw new UnauthorizedAccessException(msg); - - return false; - } - - return true; - } - - IBarcodeReaderGeneric CreateBarcodeReader(MobileBarcodeScanningOptions options) - { - var barcodeReader = new BarcodeReaderGeneric(); - - if (options.TryHarder.HasValue) - barcodeReader.Options.TryHarder = options.TryHarder.Value; - - if (options.PureBarcode.HasValue) - barcodeReader.Options.PureBarcode = options.PureBarcode.Value; - - if (!string.IsNullOrEmpty(options.CharacterSet)) - barcodeReader.Options.CharacterSet = options.CharacterSet; - - if (options.TryInverted.HasValue) - barcodeReader.TryInverted = options.TryInverted.Value; - - if (options.AutoRotate.HasValue) - barcodeReader.AutoRotate = options.AutoRotate.Value; - - if (options.PossibleFormats?.Any() ?? false) - { - barcodeReader.Options.PossibleFormats = new List(); - - foreach (var pf in options.PossibleFormats) - barcodeReader.Options.PossibleFormats.Add(pf); - } - - return barcodeReader; - } - - public void RotateCounterClockwise(byte[] source, ref byte[] target, int width, int height) - { - if (source.Length != (target?.Length ?? -1)) - target = new byte[source.Length]; - - for (int y = 0; y < height; y++) - for (int x = 0; x < width; x++) - target[x * height + height - y - 1] = source[x + y * width]; - } - - const int maxHistory = 10; // a bit arbitrary :-/ - struct LastResult - { - public long Timestamp; - public Result Result; - }; - - readonly RingBuffer _ring = new RingBuffer(maxHistory); - readonly IComparer _resultComparer = Comparer.Create((x, y) => x.Result.Text.CompareTo(y.Result.Text)); - long _delay; - - byte[] _matrix; - byte[] _rotatedMatrix; - - async public void OnPreviewFrame(IntPtr data, Camera camera) - { - System.Diagnostics.Stopwatch sw = null; - using (var buffer = new FastJavaByteArray(data)) // avoids marshalling - { - try - { + public static class IntEx + { + public static bool Between(this int i, int lower, int upper) + { + return lower <= i && i <= upper; + } + } + + public static class HandlerEx + { + public static void PostSafe(this Handler self, Action action) + { + self.Post(() => + { + try + { + action(); + } + catch (Exception ex) + { + // certain death, unless we squash + Log.Debug(MobileBarcodeScanner.TAG, $"Squashing: {ex} to avoid certain death! Handler is: {self.GetHashCode()}"); + } + }); + } + + public static void PostSafe(this Handler self, Func action) + { + self.Post(async () => + { + try + { + await action(); + } + catch (Exception ex) + { + // certain death, unless we squash + Log.Debug(MobileBarcodeScanner.TAG, $"Squashing: {ex} to avoid certain death! Handler is: {self.GetHashCode()}"); + } + }); + } + + } + + public static class RectFEx + { + public static void Flip(this RectF s) + { + var tmp = s.Left; + s.Left = s.Top; + s.Top = tmp; + tmp = s.Right; + s.Right = s.Bottom; + s.Bottom = tmp; + } + } + + class MyOrientationEventListener : OrientationEventListener + { + public MyOrientationEventListener(Context context, SensorDelay delay) : base(context, delay) { } + + public event Action OrientationChanged; + + public override void OnOrientationChanged(int orientation) + { + OrientationChanged?.Invoke(orientation); + } + } + + public class ZXingTextureView : TextureView, IScannerView, Camera.IAutoFocusCallback, INonMarshalingPreviewCallback + { + Camera.CameraInfo _cameraInfo; + Camera _camera; + + static ZXingTextureView() + { + } + + public ZXingTextureView(IntPtr javaRef, JniHandleOwnership transfer) : base(javaRef, transfer) + { + Init(); + } + + public ZXingTextureView(Context ctx) : base(ctx) + { + Init(); + } + + public ZXingTextureView(Context ctx, MobileBarcodeScanningOptions options) : base(ctx) + { + Init(); + ScanningOptions = options; + } + + public ZXingTextureView(Context ctx, IAttributeSet attr) : base(ctx, attr) + { + Init(); + } + + public ZXingTextureView(Context ctx, IAttributeSet attr, int defStyle) : base(ctx, attr, defStyle) + { + Init(); + } + + Toast _toast; + Handler _handler; + MyOrientationEventListener _orientationEventListener; + TaskCompletionSource _surfaceAvailable = new TaskCompletionSource(); + void Init() + { + _toast = Toast.MakeText(Context, string.Empty, ToastLength.Short); + + var handlerThread = new HandlerThread("ZXingTextureView"); + handlerThread.Start(); + _handler = new Handler(handlerThread.Looper); + + // We have to handle changes to screen orientation explicitly, as we cannot rely on OnConfigurationChanges + _orientationEventListener = new MyOrientationEventListener(Context, SensorDelay.Normal); + _orientationEventListener.OrientationChanged += OnOrientationChanged; + if (_orientationEventListener.CanDetectOrientation()) + _orientationEventListener.Enable(); + + SurfaceTextureAvailable += (sender, e) => + _surfaceAvailable.SetResult(e); + + SurfaceTextureSizeChanged += (sender, e) => + SetSurfaceTransform(e.Surface, e.Width, e.Height); + + SurfaceTextureDestroyed += (sender, e) => + { + ShutdownCamera(); + _surfaceAvailable = new TaskCompletionSource(); + }; + } + + Camera.Size PreviewSize { get; set; } + + int _lastOrientation; + SurfaceOrientation _lastSurfaceOrientation; + void OnOrientationChanged(int orientation) + { + try + { + // + // This code should only run when UI snaps into either portrait or landscape mode. + // At first glance we could just override OnConfigurationChanged, but unfortunately + // a rotation from landscape directly to reverse landscape won't fire an event + // (which is easily done by rotating via upside-down on many devices), because Android + // can just reuse the existing config and handle the rotation automatically .. + // + // .. except of course for camera orientation, which must handled explicitly *sigh*. + // Hurray Google, you sure suck at API design! + // + // Instead we waste some CPU by tracking orientation down to the last degree, every 200ms. + // I have yet to come up with a better way. + // + if (_camera == null) + return; + + var o = (((orientation + 45) % 360) / 90) * 90; // snap to 0, 90, 180, or 270. + if (o == _lastOrientation) + return; // fast path, no change .. + + // Actual snap is delayed, so check if we are actually rotated + var rotation = WindowManager.DefaultDisplay.Rotation; + if (rotation == _lastSurfaceOrientation) + return; // .. still no change + + _lastOrientation = o; + _lastSurfaceOrientation = rotation; + + _handler.PostSafe(() => + { + _camera?.SetDisplayOrientation(CameraOrientation(WindowManager.DefaultDisplay.Rotation)); // and finally, the interesting part *sigh* + }); + } + catch (Exception ex) + { + Log.Debug(MobileBarcodeScanner.TAG, $"Exception in OnOrientationChanged: {ex.Message}\n{ex.StackTrace}"); + } + } + + bool IsPortrait + { + get + { + var rotation = WindowManager.DefaultDisplay.Rotation; + return rotation == SurfaceOrientation.Rotation0 || rotation == SurfaceOrientation.Rotation180; + } + } + + Rectangle _area; + void SetSurfaceTransform(SurfaceTexture st, int width, int height) + { + var p = PreviewSize; + if (p == null) + return; // camera no ready yet, we will be called again later from SetupCamera. + + using (var metrics = new DisplayMetrics()) + { + #region transform + // Compensate for non-square pixels + WindowManager.DefaultDisplay.GetMetrics(metrics); + var aspectRatio = metrics.Xdpi / metrics.Ydpi; // close to 1, but rarely perfect 1 + + // Compensate for preview streams aspect ratio + aspectRatio *= (float)p.Height / p.Width; + + // Compensate for portrait mode + if (IsPortrait) + aspectRatio = 1f / aspectRatio; + + // OpenGL coordinate system goes form 0 to 1 + var transform = new Matrix(); + transform.SetScale(1f, aspectRatio * width / height); // lock on to width + + Post(() => + { + try + { + SetTransform(transform); + } + catch (ObjectDisposedException) { } // todo: What to do here?! For now we squash :-/ + }); // ensure we use the right thread when updating transform + + Log.Debug(MobileBarcodeScanner.TAG, $"Aspect ratio: {aspectRatio}, Transform: {transform}"); + + #endregion + + #region area + using (var max = new RectF(0, 0, p.Width, p.Height)) + using (var r = new RectF(max)) + { + // Calculate area of interest within preview + var inverse = new Matrix(); + transform.Invert(inverse); + + Log.Debug(MobileBarcodeScanner.TAG, $"Inverse: {inverse}"); + + var flip = IsPortrait; + if (flip) r.Flip(); + inverse.MapRect(r); + if (flip) r.Flip(); + + r.Intersect(max); // stream doesn't always fill the view! + + // Compensate for reverse mounted camera, like on the Nexus 5X. + var reverse = _cameraInfo.Orientation == 270; + if (reverse) + { + if (flip) + r.OffsetTo(p.Width - r.Right, 0); // shift area right + else + r.Offset(0, p.Height - r.Bottom); // shift are down + } + + _area = new Rectangle((int)r.Left, (int)r.Top, (int)r.Width(), (int)r.Height()); + + Log.Debug(MobileBarcodeScanner.TAG, $"Area: {_area}"); + } + #endregion + } + } + + IWindowManager _wm; + IWindowManager WindowManager + { + get + { + _wm = _wm ?? Context.GetSystemService(Context.WindowService).JavaCast(); + return _wm; + } + } + + bool? _hasTorch; + public bool HasTorch + { + get + { + if (_hasTorch.HasValue) + return _hasTorch.Value; + + var p = _camera.GetParameters(); + var supportedFlashModes = p.SupportedFlashModes; + + if (supportedFlashModes != null + && (supportedFlashModes.Contains(Camera.Parameters.FlashModeTorch) + || supportedFlashModes.Contains(Camera.Parameters.FlashModeOn))) + _hasTorch = CheckTorchPermissions(false); + + return _hasTorch.HasValue && _hasTorch.Value; + } + } + + bool _isAnalyzing; + public bool IsAnalyzing + { + get { return _isAnalyzing; } + } + + bool _isTorchOn; + public bool IsTorchOn + { + get { return _isTorchOn; } + } + + MobileBarcodeScanningOptions _scanningOptions; + IBarcodeReaderGeneric _barcodeReader; + public MobileBarcodeScanningOptions ScanningOptions + { + get { return _scanningOptions; } + set + { + _scanningOptions = value; + _barcodeReader = CreateBarcodeReader(value); + } + } + + bool _useContinuousFocus; + bool _autoFocusRunning; + public void AutoFocus() + { + _handler.PostSafe(() => + { + var camera = _camera; + if (camera == null || _autoFocusRunning || _useContinuousFocus) + return; // Allow camera to complete autofocus cycle, before trying again! + + _autoFocusRunning = true; + camera.CancelAutoFocus(); + camera.AutoFocus(this); + }); + } + + public void AutoFocus(int x, int y) + { + // todo: Needs some slightly serious math to map back to camera coordinates. + // The method used in ZXingSurfaceView is simply wrong. + AutoFocus(); + } + + public void OnAutoFocus(bool focus, Camera camera) + { + Log.Debug(MobileBarcodeScanner.TAG, $"OnAutoFocus: {focus}"); + _autoFocusRunning = false; + if (!(focus || _useContinuousFocus)) + AutoFocus(); + } + + + public void PauseAnalysis() + { + _isAnalyzing = false; + } + + public void ResumeAnalysis() + { + _isAnalyzing = true; + } + + Action _callback; + public void StartScanning(Action scanResultCallback, MobileBarcodeScanningOptions options = null) + { + _callback = scanResultCallback; + ScanningOptions = options ?? MobileBarcodeScanningOptions.Default; + + _handler.PostSafe(SetupCamera); + + ResumeAnalysis(); + } + + void OpenCamera() + { + if (_camera != null) + return; + + CheckCameraPermissions(); + + if (Build.VERSION.SdkInt >= BuildVersionCodes.Gingerbread) // Choose among multiple cameras from Gingerbread forward + { + int max = Camera.NumberOfCameras; + Log.Debug(MobileBarcodeScanner.TAG, $"Found {max} cameras"); + var requestedFacing = CameraFacing.Back; // default to back facing camera, .. + if (ScanningOptions.UseFrontCameraIfAvailable.HasValue && ScanningOptions.UseFrontCameraIfAvailable.Value) + requestedFacing = CameraFacing.Front; // .. but use front facing if available and requested + + var info = new Camera.CameraInfo(); + int idx = 0; + do + { + Camera.GetCameraInfo(idx++, info); // once again Android sucks! + } + while (info.Facing != requestedFacing && idx < max); + --idx; + + Log.Debug(MobileBarcodeScanner.TAG, $"Opening {info.Facing} facing camera: {idx}..."); + _cameraInfo = info; + _camera = Camera.Open(idx); + } + else + { + _camera = Camera.Open(); + } + + _camera.Lock(); + } + + async Task SetupCamera() + { + OpenCamera(); + + var p = _camera.GetParameters(); + p.PreviewFormat = ImageFormatType.Nv21; // YCrCb format (all Android devices must support this) + + // Android actually defines a barcode scene mode + if (p.SupportedSceneModes.Contains(Camera.Parameters.SceneModeBarcode)) // we might be lucky :-) + p.SceneMode = Camera.Parameters.SceneModeBarcode; + + // First try continuous video, then auto focus, then fixed + var supportedFocusModes = p.SupportedFocusModes; + if (supportedFocusModes.Contains(Camera.Parameters.FocusModeContinuousVideo)) + p.FocusMode = Camera.Parameters.FocusModeContinuousVideo; + else if (supportedFocusModes.Contains(Camera.Parameters.FocusModeAuto)) + p.FocusMode = Camera.Parameters.FocusModeAuto; + else if (supportedFocusModes.Contains(Camera.Parameters.FocusModeFixed)) + p.FocusMode = Camera.Parameters.FocusModeFixed; + + // Set automatic white balance if possible + if (p.SupportedWhiteBalance.Contains(Camera.Parameters.WhiteBalanceAuto)) + p.WhiteBalance = Camera.Parameters.WhiteBalanceAuto; + + // Check if we can support requested resolution .. + var availableResolutions = p.SupportedPreviewSizes.Select(s => new CameraResolution { Width = s.Width, Height = s.Height }).ToList(); + var resolution = ScanningOptions.GetResolution(availableResolutions); + + // .. If not, let's try and find a suitable one + resolution = resolution ?? availableResolutions.OrderBy(r => r.Width).FirstOrDefault(r => r.Width.Between(640, 1280) && r.Height.Between(640, 960)); + + // Hopefully a resolution was selected at some point + if (resolution != null) + p.SetPreviewSize(resolution.Width, resolution.Height); + + _camera.SetParameters(p); + + SetupTorch(_isTorchOn); + + p = _camera.GetParameters(); // refresh! + + _useContinuousFocus = p.FocusMode == Camera.Parameters.FocusModeContinuousVideo; + PreviewSize = p.PreviewSize; // get actual preview size (may differ from requested size) + var bitsPerPixel = ImageFormat.GetBitsPerPixel(p.PreviewFormat); + + Log.Debug(MobileBarcodeScanner.TAG, $"Preview size {PreviewSize.Width}x{PreviewSize.Height} with {bitsPerPixel} bits per pixel"); + + var surfaceInfo = await _surfaceAvailable.Task; + + SetSurfaceTransform(surfaceInfo.Surface, surfaceInfo.Width, surfaceInfo.Height); + + _camera.SetDisplayOrientation(CameraOrientation(WindowManager.DefaultDisplay.Rotation)); + _camera.SetPreviewTexture(surfaceInfo.Surface); + _camera.StartPreview(); + + int bufferSize = (PreviewSize.Width * PreviewSize.Height * bitsPerPixel) / 8; + using (var buffer = new FastJavaByteArray(bufferSize)) + _camera.AddCallbackBuffer(buffer); + + _camera.SetNonMarshalingPreviewCallback(this); + + // Docs suggest if Auto or Macro modes, we should invoke AutoFocus at least once + _autoFocusRunning = false; + if (!_useContinuousFocus) + AutoFocus(); + } + + public int CameraOrientation(SurfaceOrientation rotation) + { + int degrees = 0; + switch (rotation) + { + case SurfaceOrientation.Rotation0: + degrees = 0; + break; + case SurfaceOrientation.Rotation90: + degrees = 90; + break; + case SurfaceOrientation.Rotation180: + degrees = 180; + break; + case SurfaceOrientation.Rotation270: + degrees = 270; + break; + } + + // Handle front facing camera + if (_cameraInfo.Facing == CameraFacing.Front) + return (360 - ((_cameraInfo.Orientation + degrees) % 360)) % 360; // compensate for mirror + + return (_cameraInfo.Orientation - degrees + 360) % 360; + } + + void ShutdownCamera() + { + _handler.Post(() => + { + if (_camera == null) + return; + + var camera = _camera; + _camera = null; + + try + { + camera.StopPreview(); + camera.SetNonMarshalingPreviewCallback(null); + } + catch (Exception e) + { + Log.Error(MobileBarcodeScanner.TAG, e.ToString()); + } + finally + { + camera.Release(); + } + }); + } + + public void StopScanning() + { + PauseAnalysis(); + ShutdownCamera(); + } + + public void Torch(bool on) + { + if (!Context.PackageManager.HasSystemFeature(PackageManager.FeatureCameraFlash)) + { + Log.Info(MobileBarcodeScanner.TAG, "Flash not supported on this device"); + return; + } + + CheckTorchPermissions(); + + _isTorchOn = on; + if (_camera != null) // already running + SetupTorch(on); + } + + public void ToggleTorch() + { + Torch(!_isTorchOn); + } + + void SetupTorch(bool on) + { + var p = _camera.GetParameters(); + var supportedFlashModes = p.SupportedFlashModes ?? Enumerable.Empty(); + + string flashMode = null; + + if (on) + { + if (supportedFlashModes.Contains(Camera.Parameters.FlashModeTorch)) + flashMode = Camera.Parameters.FlashModeTorch; + else if (supportedFlashModes.Contains(Camera.Parameters.FlashModeOn)) + flashMode = Camera.Parameters.FlashModeOn; + } + else + { + if (supportedFlashModes.Contains(Camera.Parameters.FlashModeOff)) + flashMode = Camera.Parameters.FlashModeOff; + } + + if (!string.IsNullOrEmpty(flashMode)) + { + p.FlashMode = flashMode; + _camera.SetParameters(p); + } + } + + bool CheckCameraPermissions(bool throwOnError = true) + { + return CheckPermissions(Android.Manifest.Permission.Camera, throwOnError); + } + + bool CheckTorchPermissions(bool throwOnError = true) + { + return CheckPermissions(Android.Manifest.Permission.Flashlight, throwOnError); + } + + bool CheckPermissions(string permission, bool throwOnError = true) + { + Log.Debug(MobileBarcodeScanner.TAG, $"Checking {permission}..."); + + if (!PermissionsHandler .IsPermissionInManifest(Context, permission) + || !PermissionsHandler.IsPermissionGranted(Context, permission)) + { + var msg = $"Requires: {permission}, but was not found in your AndroidManifest.xml file."; + Log.Error(MobileBarcodeScanner.TAG, msg); + + if (throwOnError) + throw new UnauthorizedAccessException(msg); + + return false; + } + + return true; + } + + IBarcodeReaderGeneric CreateBarcodeReader(MobileBarcodeScanningOptions options) + { + var barcodeReader = new BarcodeReaderGeneric(); + + if (options == null) + return barcodeReader; + + if (options.TryHarder.HasValue) + barcodeReader.Options.TryHarder = options.TryHarder.Value; + + if (options.PureBarcode.HasValue) + barcodeReader.Options.PureBarcode = options.PureBarcode.Value; + + if (!string.IsNullOrEmpty(options.CharacterSet)) + barcodeReader.Options.CharacterSet = options.CharacterSet; + + if (options.TryInverted.HasValue) + barcodeReader.TryInverted = options.TryInverted.Value; + + if (options.AutoRotate.HasValue) + barcodeReader.AutoRotate = options.AutoRotate.Value; + + if (options.PossibleFormats?.Any() ?? false) + { + barcodeReader.Options.PossibleFormats = new List(); + + foreach (var pf in options.PossibleFormats) + barcodeReader.Options.PossibleFormats.Add(pf); + } + + return barcodeReader; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _orientationEventListener.Disable(); + _orientationEventListener.Dispose(); + _orientationEventListener = null; + } + + byte[] _buffer; + async public void OnPreviewFrame(IntPtr data, Camera camera) + { + System.Diagnostics.Stopwatch sw = null; + using (var fastArray = new FastJavaByteArray(data)) // avoids marshalling + { + try + { #if DEBUG - sw = new Stopwatch(); - sw.Start(); + sw = new Stopwatch(); + sw.Start(); #endif - if (!_isAnalyzing) - return; - - var isPortrait = IsPortrait; - - var result = await Task.Run(() => - { - LuminanceSource luminanceSource; - var fast = new FastJavaByteArrayYUVLuminanceSource(buffer, PreviewSize.Width, PreviewSize.Height, _area.Left, _area.Top, _area.Width, _area.Height); - if (isPortrait) - { - fast.CopyMatrix(ref _matrix); - RotateCounterClockwise(_matrix, ref _rotatedMatrix, _area.Width, _area.Height); - luminanceSource = new PlanarYUVLuminanceSource(_rotatedMatrix, _area.Height, _area.Width, 0, 0, _area.Height, _area.Width, false); - } - else - luminanceSource = fast; - - return _barcodeReader.Decode(luminanceSource); - }); - - if (result != null) - { - var now = Stopwatch.GetTimestamp(); - var lastResult = new LastResult { Result = result }; - int idx = _ring.FindIndex(ref lastResult, _resultComparer); - if (idx < 0 || lastResult.Timestamp + _delay < now) - { - _callback(result); - - lastResult.Timestamp = now; // update timestamp - if (idx < 0) - _ring.Add(lastResult); - else - _ring[idx] = lastResult; - } - } - else if (!_useContinuousFocus) - AutoFocus(); - } - catch (Exception ex) - { - // It is better to just skip a frame :-) .. - Log.Warn(MobileBarcodeScanner.TAG, ex.ToString()); - } - finally - { - camera.AddCallbackBuffer(buffer); // IMPORTANT! + if (!_isAnalyzing) + return; + + var isPortrait = IsPortrait; // this is checked asynchronously, so make sure to copy. + + var result = await Task.Run(() => + { + var dataWidth = PreviewSize.Width; + var dataHeight = PreviewSize.Height; + + LuminanceSource luminanceSource; + if (isPortrait) + { + fastArray.Transpose(ref _buffer, dataWidth, dataHeight); + luminanceSource = new FastJavaByteArrayYUVLuminanceSource(fastArray, dataHeight, dataWidth, _area.Top, _area.Left, _area.Height, _area.Width); + } + else + luminanceSource = new FastJavaByteArrayYUVLuminanceSource(fastArray, dataWidth, dataHeight, _area.Left, _area.Top, _area.Width, _area.Height); + + return _barcodeReader.Decode(luminanceSource); + }); + + if (result != null) + _callback(result); + else if (!_useContinuousFocus) + AutoFocus(); + } + catch (Exception ex) + { + // It is better to just skip a frame :-) .. + Log.Warn(MobileBarcodeScanner.TAG, ex.ToString()); + } + finally + { + camera.AddCallbackBuffer(fastArray); // IMPORTANT! #if DEBUG - sw.Stop(); - try - { - Post(() => - { - _toast.SetText(string.Format("{0}ms", sw.ElapsedMilliseconds)); - _toast.Show(); - }); - } - catch { } // squash + sw.Stop(); + try + { + Post(() => + { + _toast.SetText(string.Format("{0}ms", sw.ElapsedMilliseconds)); + _toast.Show(); + }); + } + catch { } // squash #endif - } - } - } - } + } + } + } + } } diff --git a/Source/ZXing.Net.Mobile.Core/MobileBarcodeScanningOptions.cs b/Source/ZXing.Net.Mobile.Core/MobileBarcodeScanningOptions.cs index ca13e9684..82ef96954 100644 --- a/Source/ZXing.Net.Mobile.Core/MobileBarcodeScanningOptions.cs +++ b/Source/ZXing.Net.Mobile.Core/MobileBarcodeScanningOptions.cs @@ -14,11 +14,10 @@ public class MobileBarcodeScanningOptions public MobileBarcodeScanningOptions () { - this.PossibleFormats = new List(); - //this.AutoRotate = true; - this.DelayBetweenAnalyzingFrames = 150; - this.InitialDelayBeforeAnalyzingFrames = 300; - this.DelayBetweenContinuousScans = 1000; + PossibleFormats = new List(); + DelayBetweenAnalyzingFrames = 150; + InitialDelayBeforeAnalyzingFrames = 300; + DelayBetweenContinuousScans = 1000; UseNativeScanning = false; } diff --git a/Source/ZXing.Net.Mobile.Forms.Android/ZXing.Net.Mobile.Forms.Android.csproj b/Source/ZXing.Net.Mobile.Forms.Android/ZXing.Net.Mobile.Forms.Android.csproj index d0dbe5868..9301bbe1d 100644 --- a/Source/ZXing.Net.Mobile.Forms.Android/ZXing.Net.Mobile.Forms.Android.csproj +++ b/Source/ZXing.Net.Mobile.Forms.Android/ZXing.Net.Mobile.Forms.Android.csproj @@ -14,7 +14,7 @@ True ZXing.Net.Mobile.Forms.Android v7.1 - v7.0 + v7.1 diff --git a/Source/ZXing.Net.Mobile.Forms.Android/ZXingScannerViewRenderer.cs b/Source/ZXing.Net.Mobile.Forms.Android/ZXingScannerViewRenderer.cs index f4ec6931d..ba30130e5 100644 --- a/Source/ZXing.Net.Mobile.Forms.Android/ZXingScannerViewRenderer.cs +++ b/Source/ZXing.Net.Mobile.Forms.Android/ZXingScannerViewRenderer.cs @@ -9,15 +9,16 @@ using Xamarin.Forms; using Xamarin.Forms.Platform.Android; -using ZXing.Mobile; using ZXing.Net.Mobile.Forms; using ZXing.Net.Mobile.Forms.Android; +using MyView = ZXing.Mobile.ZXingTextureView; + [assembly: ExportRenderer(typeof(ZXingScannerView), typeof(ZXingScannerViewRenderer))] namespace ZXing.Net.Mobile.Forms.Android { [Preserve(AllMembers = true)] - public class ZXingScannerViewRenderer : ViewRenderer + public class ZXingScannerViewRenderer : ViewRenderer { public static void Init () { @@ -27,7 +28,7 @@ public static void Init () protected ZXingScannerView formsView; - protected ZXingTextureView zxingTexture; + protected MyView view; internal Task requestPermissionsTask; protected override async void OnElementChanged(ElementChangedEventArgs e) @@ -36,15 +37,15 @@ protected override async void OnElementChanged(ElementChangedEventArgs { - if (zxingTexture != null) { + if (view != null) { if (x < 0 && y < 0) - zxingTexture.AutoFocus (); + view.AutoFocus (); else - zxingTexture.AutoFocus (x, y); + view.AutoFocus (x, y); } }; @@ -53,19 +54,19 @@ protected override async void OnElementChanged(ElementChangedEventArgs