SocketMobile / capturesdk_flutter

Public snapshot for flutter sdk
MIT License
3 stars 1 forks source link

iOS Crashes #6

Open bobekos opened 10 months ago

bobekos commented 10 months ago

We are currently using the 1.3.17 version of the sdk. Unfortunately we have some crashes on devices where the socket mobile companion app is not preinstalled (we can't guarantee that the user has installed your app). This happens only on iOS Devices. Android seems to work.

The scanning function is also not delivered globally to the app, but is only limited to a small part of the app. This is what our initialization setup looks like:

Capture? _mainInstance;
Capture? _deviceCapture;
DeviceInfo? _deviceInfo;

//setup
void asyncSetup(Function() onSuccess) async {
  try {
    _mainInstance = Capture();

     await _mainInstance!.openClient(_appInfo, (e, handle) {
      if (e is CaptureException) {
        return;
      }
      final event = e as CaptureEvent;
      switch (event.id) {
        case CaptureEventIds.deviceArrival:
          _deviceCapture = Capture();
          if (_deviceInfo == null) {
            _deviceInfo = event.deviceInfo;
            onSuccess();
          } else {
            _onDeviceReconnected(event);
          }
        case CaptureEventIds.deviceRemoval:
          _onDeviceRemoved();
        case CaptureEventIds.error:
          onError();
        case CaptureEventIds.decodedData:
          _handleData(event);
        default:
      }
    });
  } catch (e) {
    e.toLog();
  }
}

when the user is done with the scan function we dispose all the references (no catched errors here):

  void dispose() async {
    super.dispose();

    _deviceInfo = null;

    try {
      await _deviceCapture?.close();
      _deviceCapture = null;
      _mainInstance = null;
    } catch (e) {
      e.toLog();
    }
  }

This works on devices where the socket mobile app is installed and also on first try when the device has no installed companion app. In this case we got this exception (which is fine):

CaptureException
code -93
message: "Unable to open Capture"
method: "clientOpen"

But when we call the setup again, the app crashes with following content:

[FBP-iOS] handleMethodCall: isSupported
[CoreBluetooth] XPC connection invalid
[FBP-iOS] handleMethodCall: getSystemDevices
*** Terminating app due to uncaught exception 'InvalidHandleException', reason: 'No such handle valid'
*** First throw call stack:
(0x1bd844c44 0x1b694fe5c 0x1054bfa28 0x1054bfbb8 0x1054c30f8 0x1b7c0beb4 0x1b7bdffc0 0x1b7bdff50 0x1b7ba1478 0x1b7ba11ac 0x1b7ba683c 0x1c4d1afc4 0x1c4d0beac 0x1c4d1a6a4 0x1c4d1a2f4 0x1bd8d3bb8 0x1bd8b54f0 0x1bd8ba37c 0x1f84d935c 0x1bfc46f58 0x1bfc46bbc 0x104b6ca30 0x1dcdecdec)
libc++abi: terminating due to uncaught exception of type NSException
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
    frame #0: 0x00000001fbf0d578 libsystem_kernel.dylib`__pthread_kill + 8
libsystem_kernel.dylib`:
->  0x1fbf0d578 <+8>:  b.lo   0x1fbf0d598               ; <+40>
    0x1fbf0d57c <+12>: pacibsp 
    0x1fbf0d580 <+16>: stp    x29, x30, [sp, #-0x10]!
    0x1fbf0d584 <+20>: mov    x29, sp
Target 0: (Runner) stopped.
Lost connection to device.

This also happens sometimes on devices where the companion app is installed, running and the scanner is connected.

sdksupport-socketmobile commented 10 months ago

Hi @bobekos, sorry you're running into issues! Might I ask what you are logging for handle when this issue arises? Does it give you a numeric handle? Is it null? Also is this a stateful widget? I noticed you aren't using setState when setting _deviceCapture and _mainInstance to null. Also, what is the result of await _deviceCapture?.close();? Just a success message? I know sometimes with dispose there are sometimes issues with proper invocation and sometimes it needs to be called directly.

bobekos commented 10 months ago

Hi @sdksupport-socketmobile,

Might I ask what you are logging for handle when this issue arises?

Nothing. It seems that _mainInstance = Capture(); is throwing the exception. So the callback is never called.

Also is this a stateful widget?

Kind of. The setup and the logic is managed by a cubit from the bloc package. The dispose function is called when the cubit is closed. The cubit is closed when the "scanner" widget is removed from the widget tree.

Also, what is the result of await _deviceCapture?.close();

Also here the deviceCapture is null to this time, because the "deviceArrival" event never happens.

It seems like create a new instance of the capture instance is cause the error.

sdksupport-socketmobile commented 10 months ago

So the fact that there is no handle makes sense why there is an issue regarding a valid handle.

Also here the deviceCapture is null to this time, because the "deviceArrival" event never happens.

Do you mean that even when you first start the app/connect the scanner it doesn't get called? Or is this only after you've first removed the scanner and then try to reconnect?

Also what kind of device(s) are you connecting to?

bobekos commented 10 months ago

When the user calls up a configured scanner for the first time (we use the SocketMobile D-740 models), we get the following values back: _mainCapture (handle result) => -812022215 or -812949215 (it seems like the numbers are changing all the time) _deviceCapture.openDevice(deviceInfo.guid, _mainInstance) => 0 _deviceCapture?.close() => 0

Then when the user leaves and enter the "scanner" widget again and the _mainCapture instance is initialized again the crash occurs (it doesn't happen all the time). The same code is working on android.

Edit: What does the handle value means? I mean the api of this sdk is really not easy to use (for example why the response types of the eventNotification callback are dynamic? You can guess what values you expect.) and unfortunately the docs are not providing any infos about the potentially errors.

sdksupport-socketmobile commented 10 months ago

(it seems like the numbers are changing all the time)

This is the correct behavior. I can cover this more when I explain what a handle is.

_deviceCapture.openDevice(deviceInfo.guid, _mainInstance) => 0 _deviceCapture?.close() => 0

These responses are success responses.

Whenever you get a 0 it means there was no errors/any other issues. So in the above instance, the _deviceCapture was opened successfully and then it was closed successfully. The reason we return a 0 response instead of, say, an object, is because these events (and their associated objects) are handled in the event listener (_onCaptureEvent).

Then when the user leaves and enter the "scanner" widget again and the _mainCapture instance is initialized again

You might be running into an issue with re-initializing the main capture instance because it’s actually not supposed to be reinitialized. In order to reinitialize the main capture instance, you need to perform a close of the main capture instance. This could be creating a conflict because you are, in essence, trying to open a main capture instance on top of a main capture instance. If you really want to close your main capture instance (instead of just closing/removing device without closing the main thread), maybe in your dispose function you should add a line that says await _mainInstance.close(); or something like that.

Again, from what you are describing it doesn’t seem like you need to close the main instance, but just _deviceCapture.

If you look at the capture flutter sample, you’ll see in main.dart, we have _openCapture as occurring separately from _deviceOpen and it is separate from _onCaptureEvent. _openCapture() is only called in initState (when app is first loaded), and then it is also available to be called from the Footer widget because there is a button in that widget that manually closes/opens the main capture instance if you should want to do that.

What does the handle value means?

Great question! The concept of a handle is a relatively common programming term (especially with more event-driven ecosystems such as ours) used to represent a reference or identifier to an object or resource in a system. So in our SDK we use a handle to identify a specific connection/session instance.

These instances can represent a connection to a physical device (barcode scanner) or the main instance. This is a connection is a tether from your device/app to our SDK which will act as a conduit for communication (i.e. allowing you to then connect to other devices and detect event changes).

So the handle itself is different every time there is a connection/reconnection. Upon every instantiation a latest handle is generated. This ensures that the handle used in the connection is the most up to date (not cached or recycled) and allows for more efficient authentication. Think of it like a session token!

why the response types of the eventNotification callback are dynamic

At the moment we are doing it this way because you can get a few different response types from _onCaptureEvent, such as a CaptureEvent or a CaptureError.

That being said, we are always looking to improve typing in our SDK and moving forward we can work on more precisely typing the responses from this event listener. There are other features/updates we are working on at this time that take priority but it is definitely in the pipeline in the future. Your feedback here is very much appreciated!

and unfortunately the docs are not providing any infos about the potentially errors.

Apologies for not having more specific documentation regarding these errors. We are working on improving the documentation but there have been documentation updates that have recently taken priority (Socket Cam, NFC updates, etc.). We didn't realize this type of documentation was important for our user base so we greatly appreciate the feedback here from you as well! We can also try to prioritize this in the future. Again, thank you for bringing it to our attention.

What you can do now

As I recommended earlier, changing the structure of your event handling so as to not close the main instance necessarily (unless you want to directly want close it, in which case you will need to call the close on _mainInstance directly). You don't need to reinitialize the main instance every time you want to connect a device to the capture SDK.

Another course of action would be to ensure you are using a newer version of the flutter SDK. We just released a newer version recently that also offers support for SocketCam C820!

Also, feel free to refer to our capture sdk sample. It can demonstrate how/when to open/close the main instance and how to handle the events, errors, and widget state!

bobekos commented 10 months ago

@sdksupport-socketmobile

First of all, thank you for the very detailed answer. Here are a few more thoughts I would like to share with you.

Great question! The concept of a handle is a relatively common programming term (especially with more event-driven ecosystems such as ours) used to represent a reference or identifier to an object or resource in a system. So in our SDK we use a handle to identify a specific connection/session instance...

Then why is this (session) identifier even passed down to the API level? This makes the interface more confusing and offers absolutely no advantages as far as I can see. At no point where the API is used does I need the handle value (even in the example, the values are only logged as far as I could see). Since this value is unknown to me, I cannot deduce anything from it. Unless you can somehow use it for debugging purpose.

At the moment we are doing it this way because you can get a few different response types from _onCaptureEvent, such as a CaptureEvent or a CaptureError.

Here I would simply break up this abstraction by giving the API (_openClient(onEvent, onError, etc)) different callbacks. The advantage would be that I can see immediately which types I can expect and what exactly this function does. I would be type safety and I wouldn't have to do any type casting. OnCaptureEvent(CaptureEvent event)? Alright i know what to do and how handle them. OnErrorEvent(CaptureError error)? Oh something went wrong, let me check it.

As I recommended earlier, changing the structure of your event handling so as to not close the main instance necessarily (unless you want to directly want close it, in which case you will need to call the close on _mainInstance directly).

The problem is that I cannot guarantee that the user is using a SocketMobile scanner. That's why our app offers different implementations for different scanners. The resources for the respective connection should of course only be initialized when I need them. Why open a mainCapture instance if the user in this app does not use a scanner at all? I will now try to close the session completely after the dispose. Unfortunately I don't understand why this has to be a runtime exception when i try to initialize the main instance again ? You could also catch it. And why this error does not occur on Android?

Thank you again for your help. I'll take another look at the example and modify my implementation.