From d61ed46d83f2f09a42eaa3704d5155470bd04e73 Mon Sep 17 00:00:00 2001 From: "kasper@byolimit.com" Date: Tue, 23 May 2017 10:19:15 +0200 Subject: [PATCH 01/12] Add RotateInPlace method to FastJavaArrayEx, avoid code duplication in FastJavaByteArrayYUVLuminanceSource and simplify CameraAnalyzer --- .../CameraAccess/CameraAnalyzer.cs | 52 +++---------------- .../FastJavaArrayEx.cs | 29 +++++++++++ .../FastJavaByteArrayYUVLuminanceSource.cs | 33 +++--------- 3 files changed, 42 insertions(+), 72 deletions(-) diff --git a/Source/ZXing.Net.Mobile.Android/CameraAccess/CameraAnalyzer.cs b/Source/ZXing.Net.Mobile.Android/CameraAccess/CameraAnalyzer.cs index ba33d73e3..7d5012344 100644 --- a/Source/ZXing.Net.Mobile.Android/CameraAccess/CameraAnalyzer.cs +++ b/Source/ZXing.Net.Mobile.Android/CameraAccess/CameraAnalyzer.cs @@ -115,9 +115,7 @@ private void HandleOnPreviewFrameReady(object sender, FastJavaByteArray fastArra }, TaskContinuationOptions.OnlyOnFaulted); } - byte[] _matrix; - byte[] _rotatedMatrix; - + private byte[] buffer; private void DecodeFrame(FastJavaByteArray fastArray) { var cameraParameters = _cameraController.Camera.GetParameters(); @@ -126,34 +124,17 @@ private void DecodeFrame(FastJavaByteArray fastArray) InitBarcodeReaderIfNeeded(); - var rotate = false; - var newWidth = width; - var newHeight = height; - // use last value for performance gain var cDegrees = _cameraController.LastCameraDisplayOrientationDegree; + var rotate = (cDegrees == 90 || cDegrees == 270); - if (cDegrees == 90 || cDegrees == 270) - { - rotate = true; - newWidth = height; - newHeight = width; - } - - ZXing.Result result = null; + Result result = null; var start = PerformanceCounter.Start(); - LuminanceSource luminanceSource; - - var fast = new FastJavaByteArrayYUVLuminanceSource(fastArray, width, height, 0, 0, width, height); // _area.Left, _area.Top, _area.Width, _area.Height); - if (rotate) - { - fast.CopyMatrix(ref _matrix); - RotateCounterClockwise(_matrix, ref _rotatedMatrix, width, height); // _area.Width, _area.Height); - luminanceSource = new PlanarYUVLuminanceSource(_rotatedMatrix, height, width, 0, 0, height, width, false); // _area.Height, _area.Width, 0, 0, _area.Height, _area.Width, false); - } - else - luminanceSource = fast; + if (rotate) + fastArray.RotateInPlace(ref buffer, width, height); + + var luminanceSource = new FastJavaByteArrayYUVLuminanceSource(fastArray, width, height, 0, 0, width, height); // _area.Left, _area.Top, _area.Width, _area.Height); result = _barcodeReader.Decode(luminanceSource); @@ -200,24 +181,5 @@ private void InitBarcodeReaderIfNeeded() _barcodeReader.Options.PossibleFormats.Add(pf); } } - - private static byte[] RotateCounterClockwise(byte[] data, int width, int height) - { - var rotatedData = new byte[data.Length]; - for (var y = 0; y < height; y++) - for (var x = 0; x < width; x++) - rotatedData[x*height + height - y - 1] = data[x + y*width]; - return rotatedData; - } - - private 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]; - } } } \ No newline at end of file diff --git a/Source/ZXing.Net.Mobile.Android/FastJavaArrayEx.cs b/Source/ZXing.Net.Mobile.Android/FastJavaArrayEx.cs index 504663da2..267952ce4 100644 --- a/Source/ZXing.Net.Mobile.Android/FastJavaArrayEx.cs +++ b/Source/ZXing.Net.Mobile.Android/FastJavaArrayEx.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using System.Threading; using ApxLabs.FastAndroidCamera; namespace ZXing.Mobile @@ -13,5 +14,33 @@ public static void BlockCopyTo(this FastJavaByteArray self, int sourceIndex, byt Marshal.Copy(new IntPtr(self.Raw + sourceIndex), array, arrayIndex, Math.Min(length, Math.Min(self.Count, array.Length - arrayIndex))); } } + + static readonly ThreadLocal _buffer = new ThreadLocal(); + public static void RotateInPlace(this FastJavaByteArray self, int width, int height) + { + var data = _buffer.Value; + self.RotateInPlace(ref data, width, height); + _buffer.Value = data; + } + + public static void RotateInPlace(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)"); + + 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); + + 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]; + } + } } } \ No newline at end of file diff --git a/Source/ZXing.Net.Mobile.Android/FastJavaByteArrayYUVLuminanceSource.cs b/Source/ZXing.Net.Mobile.Android/FastJavaByteArrayYUVLuminanceSource.cs index 523a447df..ea9242614 100644 --- a/Source/ZXing.Net.Mobile.Android/FastJavaByteArrayYUVLuminanceSource.cs +++ b/Source/ZXing.Net.Mobile.Android/FastJavaByteArrayYUVLuminanceSource.cs @@ -42,16 +42,15 @@ public sealed class FastJavaByteArrayYUVLuminanceSource : BaseLuminanceSource private readonly int _top; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The yuv data. + /// The yuv data. /// Width of the data. /// Height of the data. /// The left. /// The top. /// The width. /// The height. - /// if set to true [reverse horiz]. public FastJavaByteArrayYUVLuminanceSource(FastJavaByteArray yuv, int dataWidth, int dataHeight, @@ -116,27 +115,8 @@ override public byte[] Matrix { get { - int width = Width; - int height = Height; - - int area = width * height; - byte[] matrix = new byte[area]; - int inputOffset = _top * _dataWidth + _left; - - // If the width matches the full width of the underlying data, perform a single copy. - if (width == _dataWidth) - { - _yuv.BlockCopyTo(inputOffset, matrix, 0, area); - return matrix; - } - - // Otherwise copy one cropped row at a time. - for (int y = 0; y < height; y++) - { - int outputOffset = y * width; - _yuv.BlockCopyTo(inputOffset, matrix, outputOffset, width); - inputOffset += _dataWidth; - } + byte[] matrix = null; + CopyMatrix(ref matrix); return matrix; } } @@ -160,7 +140,8 @@ public void CopyMatrix(ref byte[] matrix) // If the width matches the full width of the underlying data, perform a single copy. _yuv.BlockCopyTo(inputOffset, matrix, 0, area); } - else { + else + { // Otherwise copy one cropped row at a time. for (int y = 0; y < height; y++) { @@ -201,8 +182,6 @@ override public LuminanceSource crop(int left, int top, int width, int height) protected override LuminanceSource CreateLuminanceSource(byte[] newLuminances, int width, int height) { - // Called when rotating. - // todo: This partially defeats the purpose as we traffic in byte[] luminances return new PlanarYUVLuminanceSource(newLuminances, width, height, 0, 0, width, height, false); } } From 58f058e377b0420d693952801d93615d26728185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 10 Nov 2016 09:37:34 +0100 Subject: [PATCH 02/12] Use TextureView instead of SurfaceView on Android. Compared to existing ZXingSurfaceView, ZXingTextureView fixes the following: - Ensure correct aspect ratio, no matter what the view size. - Use background handler for all camera manipulation. - Crop preview to match viewfinder, so that you only scan what you see. - Avoid byte[] marshalling between Java and .Net runtime (FastJavaByteArray). - Return buffer to java runtime with camera.AddCallbackBuffer. - Avoid traslating from YCrCb to Rgb and back to luminance data (FastJavaByteArrayYUVLuminanceSource) - Re-use buffers, to avoid memory pressure, when rotating. - Automatically request autofocus on devices that does not support continous auto focus mode. - Prefer continous mode - For Xamarin.Forms ZXingScannerView now uses ZXingTextureView in the renderer on Android. --- Samples/Forms/Core/CustomScanPage.cs | 16 +- Samples/Forms/Droid/FormsSample.Droid.csproj | 3 + .../CameraExtensions.cs | 130 ++++ .../FastJavaByteArray.cs | 293 ++++++++ .../FastJavaByteArrayEnumerator.cs | 83 +++ .../FastJavaByteArrayYUVLuminanceSource.cs | 4 +- .../ZXing.Net.Mobile.Android.csproj | 4 + .../ZXingTextureView.cs | 704 ++++++++++++++++++ Source/ZXing.Net.Mobile.Core/Performance.cs | 3 +- .../ZXingScannerViewRenderer.cs | 65 +- .../ZXingScannerView.cs | 2 +- 11 files changed, 1268 insertions(+), 39 deletions(-) create mode 100644 Source/ZXing.Net.Mobile.Android/CameraExtensions.cs create mode 100644 Source/ZXing.Net.Mobile.Android/FastJavaByteArray.cs create mode 100644 Source/ZXing.Net.Mobile.Android/FastJavaByteArrayEnumerator.cs create mode 100644 Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs diff --git a/Samples/Forms/Core/CustomScanPage.cs b/Samples/Forms/Core/CustomScanPage.cs index d7f990cc6..87e302e37 100644 --- a/Samples/Forms/Core/CustomScanPage.cs +++ b/Samples/Forms/Core/CustomScanPage.cs @@ -21,6 +21,15 @@ public CustomScanPage () : base () VerticalOptions = LayoutOptions.FillAndExpand, AutomationId = "zxingScannerView", }; + + var formats = zxing.Options.PossibleFormats; + formats.Clear(); + formats.Add(ZXing.BarcodeFormat.CODE_128); + formats.Add(ZXing.BarcodeFormat.CODE_39); + formats.Add(ZXing.BarcodeFormat.QR_CODE); + + zxing.IsTorchOn = true; + zxing.OnScanResult += (result) => Device.BeginInvokeOnMainThread (async () => { @@ -30,8 +39,7 @@ public CustomScanPage () : base () // Show an alert await DisplayAlert ("Scanned Barcode", result.Text, "OK"); - // Navigate away - await Navigation.PopAsync (); + zxing.IsAnalyzing = true; }); overlay = new ZXingDefaultOverlay @@ -46,6 +54,10 @@ public CustomScanPage () : base () }; var grid = new Grid { + RowDefinitions = { + new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }, + new RowDefinition { Height = new GridLength(1, GridUnitType.Star) } + }, VerticalOptions = LayoutOptions.FillAndExpand, HorizontalOptions = LayoutOptions.FillAndExpand, }; diff --git a/Samples/Forms/Droid/FormsSample.Droid.csproj b/Samples/Forms/Droid/FormsSample.Droid.csproj index f57e8f665..63cb797d0 100644 --- a/Samples/Forms/Droid/FormsSample.Droid.csproj +++ b/Samples/Forms/Droid/FormsSample.Droid.csproj @@ -16,6 +16,7 @@ FormsSample.Droid Properties\AndroidManifest.xml v7.1 + v7.0 @@ -29,6 +30,7 @@ 4 None false + true full @@ -39,6 +41,7 @@ false false armeabi;armeabi-v7a;x86;arm64-v8a;x86_64 + true diff --git a/Source/ZXing.Net.Mobile.Android/CameraExtensions.cs b/Source/ZXing.Net.Mobile.Android/CameraExtensions.cs new file mode 100644 index 000000000..95528b4ea --- /dev/null +++ b/Source/ZXing.Net.Mobile.Android/CameraExtensions.cs @@ -0,0 +1,130 @@ +// +// Copyright (c) APX Labs, Inc. All rights reserved. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Many thanks to Jonathan Pryor from Xamarin for his assistance + +using System; +using Android.Hardware; +using Android.Runtime; + +namespace ApxLabs.FastAndroidCamera +{ + public static class CameraExtensions + { + static IntPtr id_addCallbackBuffer_arrayB; + public static void AddCallbackBuffer(this Camera self, FastJavaByteArray callbackBuffer) + { + if (id_addCallbackBuffer_arrayB == IntPtr.Zero) + id_addCallbackBuffer_arrayB = JNIEnv.GetMethodID(self.Class.Handle, "addCallbackBuffer", "([B)V"); + JNIEnv.CallVoidMethod(self.Handle, id_addCallbackBuffer_arrayB, new JValue(callbackBuffer.Handle)); + } + + static IntPtr id_setPreviewCallback_Landroid_hardware_Camera_PreviewCallback_; + public static void SetNonMarshalingPreviewCallback(this Camera self, INonMarshalingPreviewCallback cb) + { + if (id_setPreviewCallback_Landroid_hardware_Camera_PreviewCallback_ == IntPtr.Zero) + id_setPreviewCallback_Landroid_hardware_Camera_PreviewCallback_ = JNIEnv.GetMethodID(self.Class.Handle, "setPreviewCallbackWithBuffer", "(Landroid/hardware/Camera$PreviewCallback;)V"); + JNIEnv.CallVoidMethod(self.Handle, id_setPreviewCallback_Landroid_hardware_Camera_PreviewCallback_, new JValue(cb)); + } + + static IntPtr id_setOneShotPreviewCallback_Landroid_hardware_Camera_PreviewCallback_; + public static void SetNonMarshalingOneShotPreviewCallback(this Camera self, INonMarshalingPreviewCallback cb) + { + if (id_setOneShotPreviewCallback_Landroid_hardware_Camera_PreviewCallback_ == IntPtr.Zero) + id_setOneShotPreviewCallback_Landroid_hardware_Camera_PreviewCallback_ = JNIEnv.GetMethodID(self.Class.Handle, "setOneShotPreviewCallback", "(Landroid/hardware/Camera$PreviewCallback;)V"); + JNIEnv.CallVoidMethod(self.Handle, id_setOneShotPreviewCallback_Landroid_hardware_Camera_PreviewCallback_, new JValue(cb)); + } + } + + // Metadata.xml XPath interface reference: path="/api/package[@name='android.hardware']/interface[@name='Camera.PreviewCallback']" + [Register("android/hardware/Camera$PreviewCallback", "", "ApxLabs.FastAndroidCamera.INonMarshalingPreviewCallbackInvoker")] + public interface INonMarshalingPreviewCallback : IJavaObject { + + // Metadata.xml XPath method reference: path="/api/package[@name='android.hardware']/interface[@name='Camera.PreviewCallback']/method[@name='onPreviewFrame' and count(parameter)=2 and parameter[1][@type='byte[]'] and parameter[2][@type='android.hardware.Camera']]" +// [Register("onPreviewFrame", "([BLandroid/hardware/Camera;)V", "GetOnPreviewFrame_arrayBLandroid_hardware_Camera_Handler:ApxLabs.FastAndroidCamera.INonMarshalingPreviewCallbackInvoker, FastAndroidCamera, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null")] + [Register("onPreviewFrame", "([BLandroid/hardware/Camera;)V", "GetOnPreviewFrame_arrayBLandroid_hardware_Camera_Handler:ApxLabs.FastAndroidCamera.INonMarshalingPreviewCallbackInvoker, ZXingNetMobile, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null")] + void OnPreviewFrame(IntPtr data, Camera camera); + } + + [Register("android/hardware/Camera$PreviewCallback", DoNotGenerateAcw=true)] + internal class INonMarshalingPreviewCallbackInvoker : Java.Lang.Object, INonMarshalingPreviewCallback { + + static IntPtr java_class_ref = JNIEnv.FindClass("android/hardware/Camera$PreviewCallback"); + IntPtr class_ref; + + public static INonMarshalingPreviewCallback GetObject(IntPtr handle, JniHandleOwnership transfer) + { + return GetObject(handle, transfer); + } + + static IntPtr Validate(IntPtr handle) + { + if (!JNIEnv.IsInstanceOf(handle, java_class_ref)) + throw new InvalidCastException(string.Format("Unable to convert instance of type '{0}' to type '{1}'.", + JNIEnv.GetClassNameFromInstance(handle), "android.hardware.Camera.PreviewCallback")); + return handle; + } + + protected override void Dispose(bool disposing) + { + if (class_ref != IntPtr.Zero) + JNIEnv.DeleteGlobalRef(class_ref); + class_ref = IntPtr.Zero; + base.Dispose(disposing); + } + + public INonMarshalingPreviewCallbackInvoker(IntPtr handle, JniHandleOwnership transfer) + : base(Validate(handle), transfer) + { + IntPtr local_ref = JNIEnv.GetObjectClass(Handle); + class_ref = JNIEnv.NewGlobalRef(local_ref); + JNIEnv.DeleteLocalRef(local_ref); + } + + protected override IntPtr ThresholdClass { + get { return class_ref; } + } + + protected override Type ThresholdType { + get { return typeof(INonMarshalingPreviewCallbackInvoker); } + } + + static Delegate cb_onPreviewFrame_arrayBLandroid_hardware_Camera_; + #pragma warning disable 0169 + static Delegate GetOnPreviewFrame_arrayBLandroid_hardware_Camera_Handler() + { + if (cb_onPreviewFrame_arrayBLandroid_hardware_Camera_ == null) + cb_onPreviewFrame_arrayBLandroid_hardware_Camera_ = JNINativeWrapper.CreateDelegate((Action) n_OnPreviewFrame_arrayBLandroid_hardware_Camera_); + return cb_onPreviewFrame_arrayBLandroid_hardware_Camera_; + } + + static void n_OnPreviewFrame_arrayBLandroid_hardware_Camera_(IntPtr jnienv, IntPtr native__this, IntPtr native_data, IntPtr native_camera) + { + INonMarshalingPreviewCallback __this = GetObject(native__this, JniHandleOwnership.DoNotTransfer); + Camera camera = GetObject(native_camera, JniHandleOwnership.DoNotTransfer); + __this.OnPreviewFrame(native_data, camera); + } + #pragma warning restore 0169 + + IntPtr id_onPreviewFrame_arrayBLandroid_hardware_Camera_; + public void OnPreviewFrame(IntPtr data, Camera camera) + { + if (id_onPreviewFrame_arrayBLandroid_hardware_Camera_ == IntPtr.Zero) + id_onPreviewFrame_arrayBLandroid_hardware_Camera_ = JNIEnv.GetMethodID(class_ref, "onPreviewFrame", "([BLandroid/hardware/Camera;)V"); + JNIEnv.CallVoidMethod(Handle, id_onPreviewFrame_arrayBLandroid_hardware_Camera_, new JValue(data), new JValue(camera)); + } + } +} diff --git a/Source/ZXing.Net.Mobile.Android/FastJavaByteArray.cs b/Source/ZXing.Net.Mobile.Android/FastJavaByteArray.cs new file mode 100644 index 000000000..210078ddf --- /dev/null +++ b/Source/ZXing.Net.Mobile.Android/FastJavaByteArray.cs @@ -0,0 +1,293 @@ +// +// Copyright (c) APX Labs, Inc. All rights reserved. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Runtime.InteropServices; +using Android.Runtime; +using System.Collections.Generic; +using System; +using System.Diagnostics; +using System.Collections; +using Java.Interop; + +namespace ApxLabs.FastAndroidCamera +{ + /// + /// A wrapper around a Java array that reads elements directly from the pointer instead of through + /// expensive JNI calls. + /// + public sealed class FastJavaByteArray : Java.Lang.Object, IList + { + #region Constructors + + /// + /// Creates a new FastJavaByteArray with the given number of bytes reserved. + /// + /// Number of bytes to reserve + public FastJavaByteArray(int length) + { + if (length <= 0) + throw new ArgumentOutOfRangeException(); + + var arrayHandle = JniEnvironment.Arrays.NewByteArray(length).Handle; + if (arrayHandle == IntPtr.Zero) + throw new OutOfMemoryException(); + + // Retain a global reference to the byte array. NewByteArray() returns a local ref, and TransferLocalRef + // creates a new global ref to the array and deletes the local ref. + SetHandle(arrayHandle, JniHandleOwnership.TransferLocalRef); + Count = length; + + bool isCopy = false; + unsafe + { + // Get the pointer to the byte array using the global Handle + Raw = (byte*)JniEnvironment.Arrays.GetByteArrayElements(PeerReference, &isCopy); + } + + } + + /// + /// Creates a FastJavaByteArray wrapper around an existing Java/JNI byte array + /// + /// Native Java array handle + /// Whether to consider this byte array read-only + public FastJavaByteArray(IntPtr handle, bool readOnly=true) : base(handle, JniHandleOwnership.DoNotTransfer) + { + // DoNotTransfer is used to leave the incoming handle alone; that reference was created in Java, so it's + // Java's responsibility to delete it. DoNotTransfer creates a global reference to use here in the CLR + if (handle == IntPtr.Zero) + throw new ArgumentNullException("handle"); + + IsReadOnly = readOnly; + + Count = JNIEnv.GetArrayLength(Handle); + bool isCopy = false; + unsafe + { + // Get the pointer to the byte array using the global Handle + Raw = (byte*)JniEnvironment.Arrays.GetByteArrayElements(PeerReference, &isCopy); + } + } + + #endregion + + protected override void Dispose(bool disposing) + { + unsafe + { + if (Raw != null && Handle != IntPtr.Zero) // tell Java that we're done with this array + JniEnvironment.Arrays.ReleaseByteArrayElements(PeerReference, (sbyte*)Raw, IsReadOnly ? JniReleaseArrayElementsMode.Default : JniReleaseArrayElementsMode.Commit); + + Raw = null; + } + base.Dispose(disposing); + } + + #region IList Properties + + /// + /// Count of bytes + /// + public int Count { get; private set; } + + /// + /// Gets a value indicating whether this byte array is read only. + /// + /// true if read only; otherwise, false. + public bool IsReadOnly + { + get; + private set; + } + + /// + /// Indexer + /// + /// Index of byte + /// Byte at the given index + public byte this[int index] + { + get + { + if (index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException(); + } + byte retval; + unsafe + { + retval = Raw[index]; + } + return retval; + } + set + { + if (IsReadOnly) + { + throw new NotSupportedException("This FastJavaByteArray is read-only"); + } + + if (index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException(); + } + unsafe + { + Raw[index] = value; + } + } + } + + #endregion + + #region IList Methods + + /// + /// Adds a single byte to the list. Not supported + /// + /// byte to add + public void Add(byte item) + { + throw new NotSupportedException("FastJavaByteArray is fixed length"); + } + + /// + /// Not supported + /// + public void Clear() + { + throw new NotSupportedException("FastJavaByteArray is fixed length"); + } + + /// + /// Returns true if the item is found int he array + /// + /// Item to find + /// True if the item is found + public bool Contains(byte item) + { + return IndexOf(item) >= 0; + } + + /// + /// Copies the contents of the FastJavaByteArray into a byte array + /// + /// The array to copy to. + /// The zero-based index into the destination array where CopyTo should start. + public void CopyTo(byte[] array, int arrayIndex) + { + unsafe + { + Marshal.Copy(new IntPtr(Raw), array, arrayIndex, Math.Min(Count, array.Length - arrayIndex)); + } + } + + /// + /// Copies a block of the FastJavaByteArray into a byte array + /// + /// The zero-based index into the source where copy should start copying from. + /// The array to copy to. + /// The zero-based index into the destination array where copy should start copying to. + /// The length of the block to copy. + public void BlockCopyTo(int sourceIndex, byte[] array, int arrayIndex, int length) + { + unsafe + { + Marshal.Copy(new IntPtr(Raw+sourceIndex), array, arrayIndex, Math.Min(length, Math.Min(Count, array.Length - arrayIndex))); + } + } + + /// + /// Retreives enumerator + /// + /// Enumerator + [DebuggerHidden] + public IEnumerator GetEnumerator() + { + return new FastJavaByteArrayEnumerator(this); + } + + /// + /// Retreives enumerator + /// + /// Enumerator + [DebuggerHidden] + IEnumerator IEnumerable.GetEnumerator() + { + return new FastJavaByteArrayEnumerator(this); + } + + /// + /// Gets the first index of the given value + /// + /// Item to search for + /// Index of found item + public int IndexOf(byte item) + { + for (int i = 0; i < Count; ++i) + { + byte current; + unsafe + { + current = Raw[i]; + } + if (current == item) + return i; + } + return -1; + } + + /// + /// Not supported + /// + /// + /// + public void Insert(int index, byte item) + { + throw new NotSupportedException("FastJavaByteArray is fixed length"); + } + + /// + /// Not supported + /// + /// + /// + public bool Remove(byte item) + { + throw new NotSupportedException("FastJavaByteArray is fixed length"); + } + + /// + /// Not supported + /// + /// + public void RemoveAt(int index) + { + throw new NotSupportedException("FastJavaByteArray is fixed length"); + } + + #endregion + + #region Public Properties + + /// + /// Get the raw pointer to the underlying data. + /// + public unsafe byte* Raw { get; private set; } + + #endregion + } +} diff --git a/Source/ZXing.Net.Mobile.Android/FastJavaByteArrayEnumerator.cs b/Source/ZXing.Net.Mobile.Android/FastJavaByteArrayEnumerator.cs new file mode 100644 index 000000000..1e35f7fe4 --- /dev/null +++ b/Source/ZXing.Net.Mobile.Android/FastJavaByteArrayEnumerator.cs @@ -0,0 +1,83 @@ +// +// Copyright (c) APX Labs, Inc. All rights reserved. +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; + +namespace ApxLabs.FastAndroidCamera +{ + internal class FastJavaByteArrayEnumerator : IEnumerator + { + internal FastJavaByteArrayEnumerator(FastJavaByteArray arr) + { + if (arr == null) + throw new ArgumentNullException(); + + _arr = arr; + _idx = 0; + } + + public byte Current + { + get + { + byte retval; + unsafe { + // get value from pointer + retval = _arr.Raw[_idx]; + } + return retval; + } + } + + public void Dispose() + { + } + + public bool MoveNext() + { + if (_idx > _arr.Count) + return false; + + ++_idx; + + return _idx < _arr.Count; + } + + public void Reset() + { + _idx = 0; + } + + #region IEnumerator implementation + + object System.Collections.IEnumerator.Current { + get { + byte retval; + unsafe { + // get value from pointer + retval = _arr.Raw[_idx]; + } + return retval; + } + } + + #endregion + + FastJavaByteArray _arr; + int _idx; + } +} diff --git a/Source/ZXing.Net.Mobile.Android/FastJavaByteArrayYUVLuminanceSource.cs b/Source/ZXing.Net.Mobile.Android/FastJavaByteArrayYUVLuminanceSource.cs index ea9242614..e04817122 100644 --- a/Source/ZXing.Net.Mobile.Android/FastJavaByteArrayYUVLuminanceSource.cs +++ b/Source/ZXing.Net.Mobile.Android/FastJavaByteArrayYUVLuminanceSource.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright 2009 ZXing authors * Modifications copyright 2016 kasper@byolimit.com * @@ -185,4 +185,4 @@ protected override LuminanceSource CreateLuminanceSource(byte[] newLuminances, i return new PlanarYUVLuminanceSource(newLuminances, width, height, 0, 0, width, height, false); } } -} \ No newline at end of file +} 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 631c78468..d1d69f66c 100644 --- a/Source/ZXing.Net.Mobile.Android/ZXing.Net.Mobile.Android.csproj +++ b/Source/ZXing.Net.Mobile.Android/ZXing.Net.Mobile.Android.csproj @@ -63,6 +63,10 @@ + + + + diff --git a/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs b/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs new file mode 100644 index 000000000..f0308e8a8 --- /dev/null +++ b/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs @@ -0,0 +1,704 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Threading.Tasks; + +using Android.Content; +using Android.Content.PM; +using Android.Graphics; +using Android.Hardware; +using Android.OS; +using Android.Runtime; +using Android.Util; +using Android.Views; +using Android.Widget; + +using ApxLabs.FastAndroidCamera; + +using Camera = Android.Hardware.Camera; + +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 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(); + 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) => + { + _surfaceAvailable = new TaskCompletionSource(); + ShutdownCamera(); + }; + } + + 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) + { + 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 + var p = PreviewSize; + aspectRatio *= (float)p.Height / p.Width; + + // Compensate for portrait mode + if (IsPortrait) + aspectRatio = 1f / aspectRatio; + + // OpenGL coordinate system goes form 0 to 1 + Matrix transform = new Matrix(); + transform.SetScale(1f, aspectRatio * width / height); // lock on to width + + Post(() => SetTransform(transform)); // 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.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; + 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 (!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]; + } + + byte[] _matrix; + byte[] _rotatedMatrix; + Result _lastResult; + async public void OnPreviewFrame(IntPtr data, Camera camera) + { + System.Diagnostics.Stopwatch sw = null; + using (var buffer = new FastJavaByteArray(data)) // avoids marshalling + { + try + { +#if DEBUG + sw = new System.Diagnostics.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) + { + // don't raise the same barcode multiple times, unless we have seen atleast one other barcode or an empty frame + if (result.Text != _lastResult?.Text) + _callback(result); + } + else if (!_useContinuousFocus) + AutoFocus(); + + _lastResult = result; + } + catch (Exception ex) + { + // It is better to just skip a frame :-) .. + Log.Warn(MobileBarcodeScanner.TAG, ex.ToString()); + } + finally + { + camera.AddCallbackBuffer(buffer); // IMPORTANT! + +#if DEBUG + 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/Performance.cs b/Source/ZXing.Net.Mobile.Core/Performance.cs index 2cefdbdd4..62ac98389 100644 --- a/Source/ZXing.Net.Mobile.Core/Performance.cs +++ b/Source/ZXing.Net.Mobile.Core/Performance.cs @@ -43,8 +43,7 @@ public static void Stop(string guid, string msg) msg += " {0}"; if (Debugger.IsAttached) - System.Diagnostics.Debug.WriteLine (msg, elapsed.TotalMilliseconds); + Debug.WriteLine (msg, elapsed.TotalMilliseconds); } } - } \ No newline at end of file diff --git a/Source/ZXing.Net.Mobile.Forms.Android/ZXingScannerViewRenderer.cs b/Source/ZXing.Net.Mobile.Forms.Android/ZXingScannerViewRenderer.cs index f88201000..12149006a 100644 --- a/Source/ZXing.Net.Mobile.Forms.Android/ZXingScannerViewRenderer.cs +++ b/Source/ZXing.Net.Mobile.Forms.Android/ZXingScannerViewRenderer.cs @@ -1,23 +1,23 @@ -using System; -using Xamarin.Forms; -using ZXing.Net.Mobile.Forms; -using ZXing.Net.Mobile.Forms.Android; -using Android.Runtime; +using System; +using System.ComponentModel; +using System.Threading.Tasks; + using Android.App; -using Xamarin.Forms.Platform.Android; +using Android.Runtime; using Android.Views; -using System.ComponentModel; -using System.Reflection; -using Android.Widget; + +using Xamarin.Forms; +using Xamarin.Forms.Platform.Android; + using ZXing.Mobile; -using System.Threading.Tasks; -using System.Linq.Expressions; +using ZXing.Net.Mobile.Forms; +using ZXing.Net.Mobile.Forms.Android; -[assembly:ExportRenderer(typeof(ZXingScannerView), typeof(ZXingScannerViewRenderer))] +[assembly: ExportRenderer(typeof(ZXingScannerView), typeof(ZXingScannerViewRenderer))] namespace ZXing.Net.Mobile.Forms.Android { [Preserve(AllMembers = true)] - public class ZXingScannerViewRenderer : ViewRenderer + public class ZXingScannerViewRenderer : ViewRenderer { public ZXingScannerViewRenderer () : base () { @@ -31,7 +31,7 @@ public static void Init () protected ZXingScannerView formsView; - protected ZXingSurfaceView zxingSurface; + protected ZXingTextureView zxingTexture; internal Task requestPermissionsTask; protected override async void OnElementChanged(ElementChangedEventArgs e) @@ -40,15 +40,15 @@ protected override async void OnElementChanged(ElementChangedEventArgs { - if (zxingSurface != null) { + if (zxingTexture != null) { if (x < 0 && y < 0) - zxingSurface.AutoFocus (); + zxingTexture.AutoFocus (); else - zxingSurface.AutoFocus (x, y); + zxingTexture.AutoFocus (x, y); } }; @@ -57,19 +57,20 @@ protected override async void OnElementChanged(ElementChangedEventArgs Date: Wed, 16 Nov 2016 10:12:34 +0100 Subject: [PATCH 03/12] Reference FastAndroidCamera nuget instead of maintaining own copy. Add a FastJavaArrayEx.BlockCopyTo method to compensate for missing funtionality. --- .../CameraExtensions.cs | 130 -------- .../FastJavaArrayEx.cs | 4 +- .../FastJavaByteArray.cs | 293 ------------------ .../FastJavaByteArrayEnumerator.cs | 83 ----- .../ZXing.Net.Mobile.Android.csproj | 6 +- .../ZXingTextureView.cs | 52 +++- .../ZXing.Net.Mobile.Forms.Android.csproj | 5 + .../ZXingScannerViewRenderer.cs | 5 - .../packages.config | 4 +- 9 files changed, 60 insertions(+), 522 deletions(-) delete mode 100644 Source/ZXing.Net.Mobile.Android/CameraExtensions.cs delete mode 100644 Source/ZXing.Net.Mobile.Android/FastJavaByteArray.cs delete mode 100644 Source/ZXing.Net.Mobile.Android/FastJavaByteArrayEnumerator.cs diff --git a/Source/ZXing.Net.Mobile.Android/CameraExtensions.cs b/Source/ZXing.Net.Mobile.Android/CameraExtensions.cs deleted file mode 100644 index 95528b4ea..000000000 --- a/Source/ZXing.Net.Mobile.Android/CameraExtensions.cs +++ /dev/null @@ -1,130 +0,0 @@ -// -// Copyright (c) APX Labs, Inc. All rights reserved. -// -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Many thanks to Jonathan Pryor from Xamarin for his assistance - -using System; -using Android.Hardware; -using Android.Runtime; - -namespace ApxLabs.FastAndroidCamera -{ - public static class CameraExtensions - { - static IntPtr id_addCallbackBuffer_arrayB; - public static void AddCallbackBuffer(this Camera self, FastJavaByteArray callbackBuffer) - { - if (id_addCallbackBuffer_arrayB == IntPtr.Zero) - id_addCallbackBuffer_arrayB = JNIEnv.GetMethodID(self.Class.Handle, "addCallbackBuffer", "([B)V"); - JNIEnv.CallVoidMethod(self.Handle, id_addCallbackBuffer_arrayB, new JValue(callbackBuffer.Handle)); - } - - static IntPtr id_setPreviewCallback_Landroid_hardware_Camera_PreviewCallback_; - public static void SetNonMarshalingPreviewCallback(this Camera self, INonMarshalingPreviewCallback cb) - { - if (id_setPreviewCallback_Landroid_hardware_Camera_PreviewCallback_ == IntPtr.Zero) - id_setPreviewCallback_Landroid_hardware_Camera_PreviewCallback_ = JNIEnv.GetMethodID(self.Class.Handle, "setPreviewCallbackWithBuffer", "(Landroid/hardware/Camera$PreviewCallback;)V"); - JNIEnv.CallVoidMethod(self.Handle, id_setPreviewCallback_Landroid_hardware_Camera_PreviewCallback_, new JValue(cb)); - } - - static IntPtr id_setOneShotPreviewCallback_Landroid_hardware_Camera_PreviewCallback_; - public static void SetNonMarshalingOneShotPreviewCallback(this Camera self, INonMarshalingPreviewCallback cb) - { - if (id_setOneShotPreviewCallback_Landroid_hardware_Camera_PreviewCallback_ == IntPtr.Zero) - id_setOneShotPreviewCallback_Landroid_hardware_Camera_PreviewCallback_ = JNIEnv.GetMethodID(self.Class.Handle, "setOneShotPreviewCallback", "(Landroid/hardware/Camera$PreviewCallback;)V"); - JNIEnv.CallVoidMethod(self.Handle, id_setOneShotPreviewCallback_Landroid_hardware_Camera_PreviewCallback_, new JValue(cb)); - } - } - - // Metadata.xml XPath interface reference: path="/api/package[@name='android.hardware']/interface[@name='Camera.PreviewCallback']" - [Register("android/hardware/Camera$PreviewCallback", "", "ApxLabs.FastAndroidCamera.INonMarshalingPreviewCallbackInvoker")] - public interface INonMarshalingPreviewCallback : IJavaObject { - - // Metadata.xml XPath method reference: path="/api/package[@name='android.hardware']/interface[@name='Camera.PreviewCallback']/method[@name='onPreviewFrame' and count(parameter)=2 and parameter[1][@type='byte[]'] and parameter[2][@type='android.hardware.Camera']]" -// [Register("onPreviewFrame", "([BLandroid/hardware/Camera;)V", "GetOnPreviewFrame_arrayBLandroid_hardware_Camera_Handler:ApxLabs.FastAndroidCamera.INonMarshalingPreviewCallbackInvoker, FastAndroidCamera, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null")] - [Register("onPreviewFrame", "([BLandroid/hardware/Camera;)V", "GetOnPreviewFrame_arrayBLandroid_hardware_Camera_Handler:ApxLabs.FastAndroidCamera.INonMarshalingPreviewCallbackInvoker, ZXingNetMobile, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null")] - void OnPreviewFrame(IntPtr data, Camera camera); - } - - [Register("android/hardware/Camera$PreviewCallback", DoNotGenerateAcw=true)] - internal class INonMarshalingPreviewCallbackInvoker : Java.Lang.Object, INonMarshalingPreviewCallback { - - static IntPtr java_class_ref = JNIEnv.FindClass("android/hardware/Camera$PreviewCallback"); - IntPtr class_ref; - - public static INonMarshalingPreviewCallback GetObject(IntPtr handle, JniHandleOwnership transfer) - { - return GetObject(handle, transfer); - } - - static IntPtr Validate(IntPtr handle) - { - if (!JNIEnv.IsInstanceOf(handle, java_class_ref)) - throw new InvalidCastException(string.Format("Unable to convert instance of type '{0}' to type '{1}'.", - JNIEnv.GetClassNameFromInstance(handle), "android.hardware.Camera.PreviewCallback")); - return handle; - } - - protected override void Dispose(bool disposing) - { - if (class_ref != IntPtr.Zero) - JNIEnv.DeleteGlobalRef(class_ref); - class_ref = IntPtr.Zero; - base.Dispose(disposing); - } - - public INonMarshalingPreviewCallbackInvoker(IntPtr handle, JniHandleOwnership transfer) - : base(Validate(handle), transfer) - { - IntPtr local_ref = JNIEnv.GetObjectClass(Handle); - class_ref = JNIEnv.NewGlobalRef(local_ref); - JNIEnv.DeleteLocalRef(local_ref); - } - - protected override IntPtr ThresholdClass { - get { return class_ref; } - } - - protected override Type ThresholdType { - get { return typeof(INonMarshalingPreviewCallbackInvoker); } - } - - static Delegate cb_onPreviewFrame_arrayBLandroid_hardware_Camera_; - #pragma warning disable 0169 - static Delegate GetOnPreviewFrame_arrayBLandroid_hardware_Camera_Handler() - { - if (cb_onPreviewFrame_arrayBLandroid_hardware_Camera_ == null) - cb_onPreviewFrame_arrayBLandroid_hardware_Camera_ = JNINativeWrapper.CreateDelegate((Action) n_OnPreviewFrame_arrayBLandroid_hardware_Camera_); - return cb_onPreviewFrame_arrayBLandroid_hardware_Camera_; - } - - static void n_OnPreviewFrame_arrayBLandroid_hardware_Camera_(IntPtr jnienv, IntPtr native__this, IntPtr native_data, IntPtr native_camera) - { - INonMarshalingPreviewCallback __this = GetObject(native__this, JniHandleOwnership.DoNotTransfer); - Camera camera = GetObject(native_camera, JniHandleOwnership.DoNotTransfer); - __this.OnPreviewFrame(native_data, camera); - } - #pragma warning restore 0169 - - IntPtr id_onPreviewFrame_arrayBLandroid_hardware_Camera_; - public void OnPreviewFrame(IntPtr data, Camera camera) - { - if (id_onPreviewFrame_arrayBLandroid_hardware_Camera_ == IntPtr.Zero) - id_onPreviewFrame_arrayBLandroid_hardware_Camera_ = JNIEnv.GetMethodID(class_ref, "onPreviewFrame", "([BLandroid/hardware/Camera;)V"); - JNIEnv.CallVoidMethod(Handle, id_onPreviewFrame_arrayBLandroid_hardware_Camera_, new JValue(data), new JValue(camera)); - } - } -} diff --git a/Source/ZXing.Net.Mobile.Android/FastJavaArrayEx.cs b/Source/ZXing.Net.Mobile.Android/FastJavaArrayEx.cs index 267952ce4..0bb8d068b 100644 --- a/Source/ZXing.Net.Mobile.Android/FastJavaArrayEx.cs +++ b/Source/ZXing.Net.Mobile.Android/FastJavaArrayEx.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Runtime.InteropServices; using System.Threading; using ApxLabs.FastAndroidCamera; @@ -43,4 +43,4 @@ public static void RotateInPlace(this FastJavaByteArray self, ref byte[] buffer, } } } -} \ No newline at end of file +} diff --git a/Source/ZXing.Net.Mobile.Android/FastJavaByteArray.cs b/Source/ZXing.Net.Mobile.Android/FastJavaByteArray.cs deleted file mode 100644 index 210078ddf..000000000 --- a/Source/ZXing.Net.Mobile.Android/FastJavaByteArray.cs +++ /dev/null @@ -1,293 +0,0 @@ -// -// Copyright (c) APX Labs, Inc. All rights reserved. -// -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System.Runtime.InteropServices; -using Android.Runtime; -using System.Collections.Generic; -using System; -using System.Diagnostics; -using System.Collections; -using Java.Interop; - -namespace ApxLabs.FastAndroidCamera -{ - /// - /// A wrapper around a Java array that reads elements directly from the pointer instead of through - /// expensive JNI calls. - /// - public sealed class FastJavaByteArray : Java.Lang.Object, IList - { - #region Constructors - - /// - /// Creates a new FastJavaByteArray with the given number of bytes reserved. - /// - /// Number of bytes to reserve - public FastJavaByteArray(int length) - { - if (length <= 0) - throw new ArgumentOutOfRangeException(); - - var arrayHandle = JniEnvironment.Arrays.NewByteArray(length).Handle; - if (arrayHandle == IntPtr.Zero) - throw new OutOfMemoryException(); - - // Retain a global reference to the byte array. NewByteArray() returns a local ref, and TransferLocalRef - // creates a new global ref to the array and deletes the local ref. - SetHandle(arrayHandle, JniHandleOwnership.TransferLocalRef); - Count = length; - - bool isCopy = false; - unsafe - { - // Get the pointer to the byte array using the global Handle - Raw = (byte*)JniEnvironment.Arrays.GetByteArrayElements(PeerReference, &isCopy); - } - - } - - /// - /// Creates a FastJavaByteArray wrapper around an existing Java/JNI byte array - /// - /// Native Java array handle - /// Whether to consider this byte array read-only - public FastJavaByteArray(IntPtr handle, bool readOnly=true) : base(handle, JniHandleOwnership.DoNotTransfer) - { - // DoNotTransfer is used to leave the incoming handle alone; that reference was created in Java, so it's - // Java's responsibility to delete it. DoNotTransfer creates a global reference to use here in the CLR - if (handle == IntPtr.Zero) - throw new ArgumentNullException("handle"); - - IsReadOnly = readOnly; - - Count = JNIEnv.GetArrayLength(Handle); - bool isCopy = false; - unsafe - { - // Get the pointer to the byte array using the global Handle - Raw = (byte*)JniEnvironment.Arrays.GetByteArrayElements(PeerReference, &isCopy); - } - } - - #endregion - - protected override void Dispose(bool disposing) - { - unsafe - { - if (Raw != null && Handle != IntPtr.Zero) // tell Java that we're done with this array - JniEnvironment.Arrays.ReleaseByteArrayElements(PeerReference, (sbyte*)Raw, IsReadOnly ? JniReleaseArrayElementsMode.Default : JniReleaseArrayElementsMode.Commit); - - Raw = null; - } - base.Dispose(disposing); - } - - #region IList Properties - - /// - /// Count of bytes - /// - public int Count { get; private set; } - - /// - /// Gets a value indicating whether this byte array is read only. - /// - /// true if read only; otherwise, false. - public bool IsReadOnly - { - get; - private set; - } - - /// - /// Indexer - /// - /// Index of byte - /// Byte at the given index - public byte this[int index] - { - get - { - if (index < 0 || index >= Count) - { - throw new ArgumentOutOfRangeException(); - } - byte retval; - unsafe - { - retval = Raw[index]; - } - return retval; - } - set - { - if (IsReadOnly) - { - throw new NotSupportedException("This FastJavaByteArray is read-only"); - } - - if (index < 0 || index >= Count) - { - throw new ArgumentOutOfRangeException(); - } - unsafe - { - Raw[index] = value; - } - } - } - - #endregion - - #region IList Methods - - /// - /// Adds a single byte to the list. Not supported - /// - /// byte to add - public void Add(byte item) - { - throw new NotSupportedException("FastJavaByteArray is fixed length"); - } - - /// - /// Not supported - /// - public void Clear() - { - throw new NotSupportedException("FastJavaByteArray is fixed length"); - } - - /// - /// Returns true if the item is found int he array - /// - /// Item to find - /// True if the item is found - public bool Contains(byte item) - { - return IndexOf(item) >= 0; - } - - /// - /// Copies the contents of the FastJavaByteArray into a byte array - /// - /// The array to copy to. - /// The zero-based index into the destination array where CopyTo should start. - public void CopyTo(byte[] array, int arrayIndex) - { - unsafe - { - Marshal.Copy(new IntPtr(Raw), array, arrayIndex, Math.Min(Count, array.Length - arrayIndex)); - } - } - - /// - /// Copies a block of the FastJavaByteArray into a byte array - /// - /// The zero-based index into the source where copy should start copying from. - /// The array to copy to. - /// The zero-based index into the destination array where copy should start copying to. - /// The length of the block to copy. - public void BlockCopyTo(int sourceIndex, byte[] array, int arrayIndex, int length) - { - unsafe - { - Marshal.Copy(new IntPtr(Raw+sourceIndex), array, arrayIndex, Math.Min(length, Math.Min(Count, array.Length - arrayIndex))); - } - } - - /// - /// Retreives enumerator - /// - /// Enumerator - [DebuggerHidden] - public IEnumerator GetEnumerator() - { - return new FastJavaByteArrayEnumerator(this); - } - - /// - /// Retreives enumerator - /// - /// Enumerator - [DebuggerHidden] - IEnumerator IEnumerable.GetEnumerator() - { - return new FastJavaByteArrayEnumerator(this); - } - - /// - /// Gets the first index of the given value - /// - /// Item to search for - /// Index of found item - public int IndexOf(byte item) - { - for (int i = 0; i < Count; ++i) - { - byte current; - unsafe - { - current = Raw[i]; - } - if (current == item) - return i; - } - return -1; - } - - /// - /// Not supported - /// - /// - /// - public void Insert(int index, byte item) - { - throw new NotSupportedException("FastJavaByteArray is fixed length"); - } - - /// - /// Not supported - /// - /// - /// - public bool Remove(byte item) - { - throw new NotSupportedException("FastJavaByteArray is fixed length"); - } - - /// - /// Not supported - /// - /// - public void RemoveAt(int index) - { - throw new NotSupportedException("FastJavaByteArray is fixed length"); - } - - #endregion - - #region Public Properties - - /// - /// Get the raw pointer to the underlying data. - /// - public unsafe byte* Raw { get; private set; } - - #endregion - } -} diff --git a/Source/ZXing.Net.Mobile.Android/FastJavaByteArrayEnumerator.cs b/Source/ZXing.Net.Mobile.Android/FastJavaByteArrayEnumerator.cs deleted file mode 100644 index 1e35f7fe4..000000000 --- a/Source/ZXing.Net.Mobile.Android/FastJavaByteArrayEnumerator.cs +++ /dev/null @@ -1,83 +0,0 @@ -// -// Copyright (c) APX Labs, Inc. All rights reserved. -// -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System; -using System.Collections.Generic; - -namespace ApxLabs.FastAndroidCamera -{ - internal class FastJavaByteArrayEnumerator : IEnumerator - { - internal FastJavaByteArrayEnumerator(FastJavaByteArray arr) - { - if (arr == null) - throw new ArgumentNullException(); - - _arr = arr; - _idx = 0; - } - - public byte Current - { - get - { - byte retval; - unsafe { - // get value from pointer - retval = _arr.Raw[_idx]; - } - return retval; - } - } - - public void Dispose() - { - } - - public bool MoveNext() - { - if (_idx > _arr.Count) - return false; - - ++_idx; - - return _idx < _arr.Count; - } - - public void Reset() - { - _idx = 0; - } - - #region IEnumerator implementation - - object System.Collections.IEnumerator.Current { - get { - byte retval; - unsafe { - // get value from pointer - retval = _arr.Raw[_idx]; - } - return retval; - } - } - - #endregion - - FastJavaByteArray _arr; - int _idx; - } -} 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 d1d69f66c..71d1e1f5a 100644 --- a/Source/ZXing.Net.Mobile.Android/ZXing.Net.Mobile.Android.csproj +++ b/Source/ZXing.Net.Mobile.Android/ZXing.Net.Mobile.Android.csproj @@ -13,6 +13,7 @@ ZXing.Mobile ZXingNetMobile v4.0.3 + False @@ -43,6 +44,7 @@ ..\..\packages\FastAndroidCamera.2.0.0\lib\MonoAndroid403\FastAndroidCamera.dll + ..\..\Samples\Forms\packages\FastAndroidCamera.2.0.0\lib\MonoAndroid403\FastAndroidCamera.dll @@ -64,10 +66,8 @@ - - - + diff --git a/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs b/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs index f0308e8a8..71a3239db 100644 --- a/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs +++ b/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs @@ -8,6 +8,7 @@ using Android.Content.PM; using Android.Graphics; using Android.Hardware; +using Android.Opengl; using Android.OS; using Android.Runtime; using Android.Util; @@ -16,7 +17,10 @@ using ApxLabs.FastAndroidCamera; +using Javax.Microedition.Khronos.Egl; + using Camera = Android.Hardware.Camera; +using Matrix = Android.Graphics.Matrix; namespace ZXing.Mobile { @@ -119,6 +123,7 @@ public ZXingTextureView(Context ctx, IAttributeSet attr, int defStyle) : base(ct Handler _handler; MyOrientationEventListener _orientationEventListener; TaskCompletionSource _surfaceAvailable = new TaskCompletionSource(); + SurfaceTexture _surfaceTexture; void Init() { _toast = Toast.MakeText(Context, string.Empty, ToastLength.Short); @@ -134,17 +139,16 @@ void Init() _orientationEventListener.Enable(); SurfaceTextureAvailable += (sender, e) => - { _surfaceAvailable.SetResult(e); - }; SurfaceTextureSizeChanged += (sender, e) => SetSurfaceTransform(e.Surface, e.Width, e.Height); SurfaceTextureDestroyed += (sender, e) => { - _surfaceAvailable = new TaskCompletionSource(); ShutdownCamera(); + _surfaceAvailable = new TaskCompletionSource(); + _surfaceTexture = null; }; } @@ -214,7 +218,7 @@ void SetSurfaceTransform(SurfaceTexture st, int width, int height) aspectRatio = 1f / aspectRatio; // OpenGL coordinate system goes form 0 to 1 - Matrix transform = new Matrix(); + var transform = new Matrix(); transform.SetScale(1f, aspectRatio * width / height); // lock on to width Post(() => SetTransform(transform)); // ensure we use the right thread when updating transform @@ -438,6 +442,8 @@ async Task SetupCamera() 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)); @@ -496,6 +502,7 @@ void ShutdownCamera() { camera.StopPreview(); camera.SetNonMarshalingPreviewCallback(null); + ClearSurface(_surfaceTexture); } catch (Exception e) { @@ -508,6 +515,43 @@ void ShutdownCamera() }); } + 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(); 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 d2ba91aa9..46d52c568 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,8 +14,10 @@ True ZXing.Net.Mobile.Forms.Android v7.1 + v7.0 + true @@ -92,6 +94,9 @@ ..\..\Samples\Forms\packages\FastAndroidCamera.2.0.0\lib\MonoAndroid403\FastAndroidCamera.dll + + ..\..\Samples\Forms\packages\FastAndroidCamera.2.0.0\lib\MonoAndroid403\FastAndroidCamera.dll + diff --git a/Source/ZXing.Net.Mobile.Forms.Android/ZXingScannerViewRenderer.cs b/Source/ZXing.Net.Mobile.Forms.Android/ZXingScannerViewRenderer.cs index 12149006a..f4ec6931d 100644 --- a/Source/ZXing.Net.Mobile.Forms.Android/ZXingScannerViewRenderer.cs +++ b/Source/ZXing.Net.Mobile.Forms.Android/ZXingScannerViewRenderer.cs @@ -19,10 +19,6 @@ namespace ZXing.Net.Mobile.Forms.Android [Preserve(AllMembers = true)] public class ZXingScannerViewRenderer : ViewRenderer { - public ZXingScannerViewRenderer () : base () - { - } - public static void Init () { // Keep linker from stripping empty method @@ -57,7 +53,6 @@ protected override async void OnElementChanged(ElementChangedEventArgs + @@ -10,4 +10,4 @@ - \ No newline at end of file + From c565bff10ac3372fbbf1c59aec0892391784d451 Mon Sep 17 00:00:00 2001 From: "kasper@byolimit.com" Date: Wed, 16 Nov 2016 10:48:35 +0100 Subject: [PATCH 04/12] Updated .nuspecs to reference FastAndroidCamera nuget --- ZXing.Net.Mobile.Forms.nuspec | 3 ++- ZXing.Net.Mobile.nuspec | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ZXing.Net.Mobile.Forms.nuspec b/ZXing.Net.Mobile.Forms.nuspec index 8445fbd79..ef0b0bff6 100644 --- a/ZXing.Net.Mobile.Forms.nuspec +++ b/ZXing.Net.Mobile.Forms.nuspec @@ -1,4 +1,4 @@ - + ZXing.Net.Mobile.Forms @@ -23,6 +23,7 @@ + diff --git a/ZXing.Net.Mobile.nuspec b/ZXing.Net.Mobile.nuspec index 42376c294..4e772ef13 100644 --- a/ZXing.Net.Mobile.nuspec +++ b/ZXing.Net.Mobile.nuspec @@ -1,4 +1,4 @@ - + ZXing.Net.Mobile From 94fb8186d962317d5c199115bd94cb98b6a4dac2 Mon Sep 17 00:00:00 2001 From: "kasper@byolimit.com" Date: Wed, 16 Nov 2016 13:57:25 +0100 Subject: [PATCH 05/12] Bitten by a bad HintPath.. Doh! --- .../ZXing.Net.Mobile.Android.csproj | 2 +- .../ZXing.Net.Mobile.Forms.Android.csproj | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) 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 71d1e1f5a..57f115f85 100644 --- a/Source/ZXing.Net.Mobile.Android/ZXing.Net.Mobile.Android.csproj +++ b/Source/ZXing.Net.Mobile.Android/ZXing.Net.Mobile.Android.csproj @@ -92,4 +92,4 @@ - \ No newline at end of file + 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 46d52c568..479af3715 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 @@ -95,7 +95,7 @@ ..\..\Samples\Forms\packages\FastAndroidCamera.2.0.0\lib\MonoAndroid403\FastAndroidCamera.dll - ..\..\Samples\Forms\packages\FastAndroidCamera.2.0.0\lib\MonoAndroid403\FastAndroidCamera.dll + ..\..\packages\FastAndroidCamera.2.0.0\lib\MonoAndroid403\FastAndroidCamera.dll @@ -141,4 +141,6 @@ - \ No newline at end of file + + + From 52c64c2b17a0ce54749a698b857bdd94cac2457d Mon Sep 17 00:00:00 2001 From: "kasper@byolimit.com" Date: Tue, 17 Jan 2017 11:25:14 +0100 Subject: [PATCH 06/12] Fix: NRE in SetSurfaceTransform, if called before camera is ready. Keep a ringbuffer of recent barcodes, and honor DelayBetweenContinuousScans (of same barcode) --- Samples/Forms/Core/CustomScanPage.cs | 4 +- .../ZXingTextureView.cs | 107 ++++++++++++++++-- 2 files changed, 103 insertions(+), 8 deletions(-) diff --git a/Samples/Forms/Core/CustomScanPage.cs b/Samples/Forms/Core/CustomScanPage.cs index 87e302e37..2272b5838 100644 --- a/Samples/Forms/Core/CustomScanPage.cs +++ b/Samples/Forms/Core/CustomScanPage.cs @@ -28,13 +28,15 @@ 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.IsTorchOn = true; zxing.OnScanResult += (result) => Device.BeginInvokeOnMainThread (async () => { // Stop analysis until we navigate away so we don't keep reading barcodes - zxing.IsAnalyzing = false; + zxing.IsAnalyzing = true; // Show an alert await DisplayAlert ("Scanned Barcode", result.Text, "OK"); diff --git a/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs b/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs index 71a3239db..271f51f7b 100644 --- a/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs +++ b/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Drawing; using System.Linq; using System.Threading.Tasks; @@ -91,6 +92,75 @@ public override void OnOrientationChanged(int 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; @@ -202,6 +272,10 @@ bool IsPortrait { 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 @@ -210,7 +284,6 @@ void SetSurfaceTransform(SurfaceTexture st, int width, int height) var aspectRatio = metrics.Xdpi / metrics.Ydpi; // close to 1, but rarely perfect 1 // Compensate for preview streams aspect ratio - var p = PreviewSize; aspectRatio *= (float)p.Height / p.Width; // Compensate for portrait mode @@ -312,6 +385,7 @@ public MobileBarcodeScanningOptions ScanningOptions set { _scanningOptions = value; + _delay = TimeSpan.FromMilliseconds(value.DelayBetweenContinuousScans).Ticks; _barcodeReader = CreateBarcodeReader(value); } } @@ -674,9 +748,20 @@ public void RotateCounterClockwise(byte[] source, ref byte[] target, int width, 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; - Result _lastResult; + async public void OnPreviewFrame(IntPtr data, Camera camera) { System.Diagnostics.Stopwatch sw = null; @@ -685,7 +770,7 @@ async public void OnPreviewFrame(IntPtr data, Camera camera) try { #if DEBUG - sw = new System.Diagnostics.Stopwatch(); + sw = new Stopwatch(); sw.Start(); #endif if (!_isAnalyzing) @@ -711,14 +796,22 @@ async public void OnPreviewFrame(IntPtr data, Camera camera) if (result != null) { - // don't raise the same barcode multiple times, unless we have seen atleast one other barcode or an empty frame - if (result.Text != _lastResult?.Text) + 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(); - - _lastResult = result; } catch (Exception ex) { From 6ce17295240075b3fff16900e8f8868240a23853 Mon Sep 17 00:00:00 2001 From: "kasper@byolimit.com" Date: Tue, 17 Jan 2017 14:21:54 +0100 Subject: [PATCH 07/12] Guard against ObjectDisposedException in Post call --- .../ZXing.Net.Mobile.Android.csproj | 4 +--- Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs | 8 +++++++- .../ZXing.Net.Mobile.Forms.Android.csproj | 5 ----- 3 files changed, 8 insertions(+), 9 deletions(-) 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 57f115f85..d4d911c81 100644 --- a/Source/ZXing.Net.Mobile.Android/ZXing.Net.Mobile.Android.csproj +++ b/Source/ZXing.Net.Mobile.Android/ZXing.Net.Mobile.Android.csproj @@ -44,7 +44,6 @@ ..\..\packages\FastAndroidCamera.2.0.0\lib\MonoAndroid403\FastAndroidCamera.dll - ..\..\Samples\Forms\packages\FastAndroidCamera.2.0.0\lib\MonoAndroid403\FastAndroidCamera.dll @@ -64,10 +63,9 @@ - - + diff --git a/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs b/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs index 271f51f7b..e1dcd93a4 100644 --- a/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs +++ b/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs @@ -294,7 +294,13 @@ void SetSurfaceTransform(SurfaceTexture st, int width, int height) var transform = new Matrix(); transform.SetScale(1f, aspectRatio * width / height); // lock on to width - Post(() => SetTransform(transform)); // ensure we use the right thread when updating transform + 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}"); 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 479af3715..d0dbe5868 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 @@ -91,9 +91,6 @@ ..\..\packages\Xamarin.Forms.2.3.3.193\lib\MonoAndroid10\Xamarin.Forms.Xaml.dll - - ..\..\Samples\Forms\packages\FastAndroidCamera.2.0.0\lib\MonoAndroid403\FastAndroidCamera.dll - ..\..\packages\FastAndroidCamera.2.0.0\lib\MonoAndroid403\FastAndroidCamera.dll @@ -141,6 +138,4 @@ - - From 7d60a2012e85f88d9772a3e4041f5b3a12834ef4 Mon Sep 17 00:00:00 2001 From: "kasper@byolimit.com" Date: Wed, 31 May 2017 04:31:56 +0200 Subject: [PATCH 08/12] 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 Date: Wed, 31 May 2017 05:26:56 +0200 Subject: [PATCH 09/12] Move try-catch on OnOrientationChanged to MyOrientationListener --- .../ZXingTextureView.cs | 73 +++++++++---------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs b/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs index 437b95337..f7f5b86f8 100644 --- a/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs +++ b/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs @@ -86,7 +86,14 @@ public MyOrientationEventListener(Context context, SensorDelay delay) : base(con public override void OnOrientationChanged(int orientation) { - OrientationChanged?.Invoke(orientation); + try + { + OrientationChanged?.Invoke(orientation); + } + catch (Exception ex) + { + Log.Warn(MobileBarcodeScanner.TAG, $"Squashing {ex} in OnOrientationChanged!"); + } } } @@ -162,45 +169,38 @@ void Init() 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; + // + // 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 .. + 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 + // 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; + _lastOrientation = o; + _lastSurfaceOrientation = rotation; - _handler.PostSafe(() => - { - _camera?.SetDisplayOrientation(CameraOrientation(WindowManager.DefaultDisplay.Rotation)); // and finally, the interesting part *sigh* - }); - } - catch (Exception ex) + _handler.PostSafe(() => { - Log.Debug(MobileBarcodeScanner.TAG, $"Exception in OnOrientationChanged: {ex.Message}\n{ex.StackTrace}"); - } + _camera?.SetDisplayOrientation(CameraOrientation(WindowManager.DefaultDisplay.Rotation)); // and finally, the interesting part *sigh* + }); } bool IsPortrait @@ -370,7 +370,6 @@ public void OnAutoFocus(bool focus, Camera camera) AutoFocus(); } - public void PauseAnalysis() { _isAnalyzing = false; @@ -614,7 +613,7 @@ bool CheckPermissions(string permission, bool throwOnError = true) { Log.Debug(MobileBarcodeScanner.TAG, $"Checking {permission}..."); - if (!PermissionsHandler .IsPermissionInManifest(Context, permission) + if (!PermissionsHandler.IsPermissionInManifest(Context, permission) || !PermissionsHandler.IsPermissionGranted(Context, permission)) { var msg = $"Requires: {permission}, but was not found in your AndroidManifest.xml file."; From abff866f481ed71b68478f6cc2e2a36786b3fed1 Mon Sep 17 00:00:00 2001 From: "kasper@byolimit.com" Date: Wed, 31 May 2017 05:46:41 +0200 Subject: [PATCH 10/12] Reorder Dispose on ZXingTextureView --- Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs b/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs index f7f5b86f8..7f86552c0 100644 --- a/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs +++ b/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs @@ -663,10 +663,10 @@ IBarcodeReaderGeneric CreateBarcodeReader(MobileBarcodeScanni protected override void Dispose(bool disposing) { - base.Dispose(disposing); - _orientationEventListener.Disable(); - _orientationEventListener.Dispose(); + _orientationEventListener?.Disable(); + _orientationEventListener?.Dispose(); _orientationEventListener = null; + base.Dispose(disposing); } byte[] _buffer; From 454551bd5d4797cd14f0559ee1c43211203fc1ff Mon Sep 17 00:00:00 2001 From: "kasper@byolimit.com" Date: Thu, 15 Jun 2017 12:39:16 +0200 Subject: [PATCH 11/12] Honor DelayBetweenAnalyzingFrames & DelayBetweenContinuousScans --- .../ZXingTextureView.cs | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs b/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs index 7f86552c0..67f7c9a39 100644 --- a/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs +++ b/Source/ZXing.Net.Mobile.Android/ZXingTextureView.cs @@ -669,7 +669,32 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - byte[] _buffer; + private bool _wasScanned; + private DateTime _lastPreviewAnalysis; + private bool CanAnalyzeFrame + { + get + { + if (!IsAnalyzing) + return false; + + var elapsedTimeMs = (DateTime.UtcNow - _lastPreviewAnalysis).TotalMilliseconds; + if (elapsedTimeMs < _scanningOptions.DelayBetweenAnalyzingFrames) + return false; + + // Delay a minimum between scans + if (_wasScanned && elapsedTimeMs < _scanningOptions.DelayBetweenContinuousScans) + return false; + + // reset! + _wasScanned = false; + _lastPreviewAnalysis = DateTime.UtcNow; + + return true; + } + } + + byte[] _buffer; async public void OnPreviewFrame(IntPtr data, Camera camera) { System.Diagnostics.Stopwatch sw = null; @@ -681,7 +706,7 @@ async public void OnPreviewFrame(IntPtr data, Camera camera) sw = new Stopwatch(); sw.Start(); #endif - if (!_isAnalyzing) + if (!CanAnalyzeFrame) return; var isPortrait = IsPortrait; // this is checked asynchronously, so make sure to copy. @@ -703,8 +728,11 @@ async public void OnPreviewFrame(IntPtr data, Camera camera) return _barcodeReader.Decode(luminanceSource); }); - if (result != null) - _callback(result); + if (result != null) + { + _wasScanned = true; + _callback(result); + } else if (!_useContinuousFocus) AutoFocus(); } From dda2ff2a7defe27e88117822e77dacd63db5ebb4 Mon Sep 17 00:00:00 2001 From: "kasper@byolimit.com" Date: Thu, 15 Jun 2017 12:40:14 +0200 Subject: [PATCH 12/12] Update XF to latest stable (2.3.4) --- .../ZXing.Net.Mobile.Forms.Android.csproj | 30 +++++++++---------- .../packages.config | 6 ++-- .../ZXing.Net.Mobile.Forms.iOS.csproj | 24 +++++++-------- .../packages.config | 2 +- .../ZXing.Net.Mobile.Forms.csproj | 18 +++++------ Source/ZXing.Net.Mobile.Forms/packages.config | 2 +- 6 files changed, 41 insertions(+), 41 deletions(-) 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 9301bbe1d..cb8f353e8 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 @@ -40,9 +40,6 @@ false - - ..\..\packages\Xamarin.Forms.2.3.3.193\lib\MonoAndroid10\FormsViewGroup.dll - @@ -79,20 +76,23 @@ ..\..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\lib\MonoAndroid403\Xamarin.Android.Support.Vector.Drawable.dll True - - ..\..\packages\Xamarin.Forms.2.3.3.193\lib\MonoAndroid10\Xamarin.Forms.Core.dll + + ..\..\packages\FastAndroidCamera.2.0.0\lib\MonoAndroid403\FastAndroidCamera.dll + + + ..\..\packages\Xamarin.Forms.2.3.4.247\lib\MonoAndroid10\FormsViewGroup.dll - - ..\..\packages\Xamarin.Forms.2.3.3.193\lib\MonoAndroid10\Xamarin.Forms.Platform.dll + + ..\..\packages\Xamarin.Forms.2.3.4.247\lib\MonoAndroid10\Xamarin.Forms.Core.dll - - ..\..\packages\Xamarin.Forms.2.3.3.193\lib\MonoAndroid10\Xamarin.Forms.Platform.Android.dll + + ..\..\packages\Xamarin.Forms.2.3.4.247\lib\MonoAndroid10\Xamarin.Forms.Platform.Android.dll - - ..\..\packages\Xamarin.Forms.2.3.3.193\lib\MonoAndroid10\Xamarin.Forms.Xaml.dll + + ..\..\packages\Xamarin.Forms.2.3.4.247\lib\MonoAndroid10\Xamarin.Forms.Platform.dll - - ..\..\packages\FastAndroidCamera.2.0.0\lib\MonoAndroid403\FastAndroidCamera.dll + + ..\..\packages\Xamarin.Forms.2.3.4.247\lib\MonoAndroid10\Xamarin.Forms.Xaml.dll @@ -135,7 +135,7 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - + + diff --git a/Source/ZXing.Net.Mobile.Forms.Android/packages.config b/Source/ZXing.Net.Mobile.Forms.Android/packages.config index 3d818aa07..6b61e53e7 100644 --- a/Source/ZXing.Net.Mobile.Forms.Android/packages.config +++ b/Source/ZXing.Net.Mobile.Forms.Android/packages.config @@ -1,4 +1,4 @@ - + @@ -9,5 +9,5 @@ - - + + \ No newline at end of file diff --git a/Source/ZXing.Net.Mobile.Forms.iOS/ZXing.Net.Mobile.Forms.iOS.csproj b/Source/ZXing.Net.Mobile.Forms.iOS/ZXing.Net.Mobile.Forms.iOS.csproj index 47ada2301..5cbcfa890 100644 --- a/Source/ZXing.Net.Mobile.Forms.iOS/ZXing.Net.Mobile.Forms.iOS.csproj +++ b/Source/ZXing.Net.Mobile.Forms.iOS/ZXing.Net.Mobile.Forms.iOS.csproj @@ -34,19 +34,19 @@ - - ..\..\packages\Xamarin.Forms.2.3.3.193\lib\Xamarin.iOS10\Xamarin.Forms.Core.dll + + + ..\..\packages\Xamarin.Forms.2.3.4.247\lib\Xamarin.iOS10\Xamarin.Forms.Core.dll - - ..\..\packages\Xamarin.Forms.2.3.3.193\lib\Xamarin.iOS10\Xamarin.Forms.Platform.dll + + ..\..\packages\Xamarin.Forms.2.3.4.247\lib\Xamarin.iOS10\Xamarin.Forms.Platform.dll - - ..\..\packages\Xamarin.Forms.2.3.3.193\lib\Xamarin.iOS10\Xamarin.Forms.Platform.iOS.dll + + ..\..\packages\Xamarin.Forms.2.3.4.247\lib\Xamarin.iOS10\Xamarin.Forms.Platform.iOS.dll - - ..\..\packages\Xamarin.Forms.2.3.3.193\lib\Xamarin.iOS10\Xamarin.Forms.Xaml.dll + + ..\..\packages\Xamarin.Forms.2.3.4.247\lib\Xamarin.iOS10\Xamarin.Forms.Xaml.dll - @@ -83,7 +83,7 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - \ No newline at end of file + + + diff --git a/Source/ZXing.Net.Mobile.Forms.iOS/packages.config b/Source/ZXing.Net.Mobile.Forms.iOS/packages.config index 894c7ac89..5f8f73cbd 100644 --- a/Source/ZXing.Net.Mobile.Forms.iOS/packages.config +++ b/Source/ZXing.Net.Mobile.Forms.iOS/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/Source/ZXing.Net.Mobile.Forms/ZXing.Net.Mobile.Forms.csproj b/Source/ZXing.Net.Mobile.Forms/ZXing.Net.Mobile.Forms.csproj index 6d29c648f..04dd131c9 100644 --- a/Source/ZXing.Net.Mobile.Forms/ZXing.Net.Mobile.Forms.csproj +++ b/Source/ZXing.Net.Mobile.Forms/ZXing.Net.Mobile.Forms.csproj @@ -51,14 +51,14 @@ - - ..\..\packages\Xamarin.Forms.2.3.3.193\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Core.dll + + ..\..\packages\Xamarin.Forms.2.3.4.247\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Core.dll - - ..\..\packages\Xamarin.Forms.2.3.3.193\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Platform.dll + + ..\..\packages\Xamarin.Forms.2.3.4.247\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Platform.dll - - ..\..\packages\Xamarin.Forms.2.3.3.193\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Xaml.dll + + ..\..\packages\Xamarin.Forms.2.3.4.247\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Xaml.dll @@ -68,7 +68,7 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - \ No newline at end of file + + + diff --git a/Source/ZXing.Net.Mobile.Forms/packages.config b/Source/ZXing.Net.Mobile.Forms/packages.config index fd571b304..e18960ffb 100644 --- a/Source/ZXing.Net.Mobile.Forms/packages.config +++ b/Source/ZXing.Net.Mobile.Forms/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file