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