UnexpectedTypeException: Expected a Toybox.Lang.Number or WatchUi.BitmapResource

One of my users reported this error, but unfortunately I don’t have a stack trace to go on.
I’m hoping to tap into the swarm intelligence here - does anyone know of an SDK function that accepts a parameter which can be either a Number or a BitmapResource?
I’ve checked my own code and don’t see any instances of this.

  • There are lots of typedefs that are based on Number. For example Symbol, ResourceId, Color value, ... Do check all the places where one of the expected types is ButmapResource 

  • Yes, good point. After considering this, I initially suspected that a BufferedBitmap passed as the :bitmap option into the constructor of a Bitmap drawable might be causing the issue.

    However, when I intentionally pass a String instead (for testing), I get a different error:
    UnexpectedTypeException: Expected ResourceId, given String.

    So while the cause may still be related, it's not conclusive. What I’m doing there is a bit unconventional: on CIQ versions prior to 4.0.0, a BufferedBitmap can’t be passed directly into a BitmapDrawable because it doesn’t implement getHeight() and getWidth(). To work around this, I derived a custom class from BufferedBitmap that adds those functions. This approach works in the simulator, but it’s possible that it causes problems on real devices - especially since the user is on a Fenix 6 Pro, which runs a CIQ version prior to 4.0.0.

    That said, this is just a hunch - the root cause might be entirely different. I’ll investigate further.

  • OK, my hunch was right.

    The issue I'm facing is this: I need a Drawable that I can render myself by drawing primitives onto a bitmap.

    The simplest approach seemed to be:

    1. Create a BufferedBitmap,

    2. Draw onto its Dc,

    3. Pass it into a Bitmap constructor (since Bitmap is a Drawable).

    This works perfectly fine in CIQ 4.0.0 and later.

    However, in earlier versions, BufferedBitmap doesn't implement getWidth() and getHeight(). Although the compiler accepts passing it into a Bitmap, at runtime it throws an exception because those methods are missing.

    To work around this, I created a subclass of BufferedBitmap that implements getWidth() and getHeight(). It works fine in the simulator — but on an actual device, the Bitmap constructor throws an UnexpectedTypeException. Apparently, it expects a real BufferedBitmap and not a subclass.

    Has anyone found a way to create a Bitmap from a BufferedBitmap on devices running CIQ versions earlier than 4.0.0?

  • TL;DR I don't think Fenix 6 Pro (and other older devices) even support initializing Bitmap with a BufferedBitmap at all. (Even older devices did not even support initializing Bitmap with a BitmapResource; they only supported the use of a resource ID)

    Unfortunately this doesn't seem to be reflected in the doc or the sim.

    I think your particular crash is happening in Bitmap.setBitmap(), called from Bitmap.initialize(). Fenix 6 Pro knows about the :bitmap option in initialize(), so it's being passed to setBitmap(). But I don't think setBitmap() accepts a BufferedBitmap, on Fenix 6 Pro (CIQ 3.4.*) I think it only accepts a resource ID (symbol/number) or BitmapResource, which would explain your exception.

    Detailed blog post:

    (the main point is that the functionality of Bitmap.initialize / setBitmap has changed a lot over time, and I don't think the docs reflect that older devices don't have all of the changes)

    However, in earlier versions, BufferedBitmap doesn't implement getWidth() and getHeight(). Although the compiler accepts passing it into a Bitmap, at runtime it throws an exception because those methods are missing

    Yeah, I can recreate this problem in the sim (fr935).

    I see a stack trace which shows a crash in Bitmap.setBitmap(), which is called by Bitmap.initialize(). (Ofc both lines of code are in the API, not in the app.)

    [1/x]

  • But on an actual device, the Bitmap constructor throws an UnexpectedTypeException. Apparently, it expects a real BufferedBitmap and not a subclass.

    I bet the real problem is that Bitmap.initialize()'s options dictionary doesn't support accepting a BufferedBitmap at all, for Fenix 6 Pro. I doubt that it's even possible for the code on the device to know that you passed in a subclass, unless it uses some private language feature that we don't know about. (The standard language feature for determining runtime types - instanceof - shouldn't care if you pass in a subclass of X when testing for X. If the API code is instead using "has" to check for identifying members, I'd say the the same logic applies)

    I tried something similar to what you did with with fr935 (real and simulated). (fr935 is on CIQ 3.1.6). I just modded the Analog SDK sample to pass one of the buffered bitrmaps into the Bitmap constructor and draw the bitmap itself, after subclassing BufferedBitmap in the same way that you did.

    It seems to work in the sim, but on a real fr935, it just results in a corrupt screen.

    Indeed, here's the CIQ 3.1.6 API doc for Bitmap.initialize():

    [this was back when the SDK and CIQ versions were synchronized]

    Note that lack of a :bitmap option at all.

    [2/x]

  • Ofc we are talking about Fenix 6 pro, which has CIQ 3.4.5. Here we have to get into the weeds of Garmin's "System Level" versioning scheme (which has confused literally everyone from end users to bloggers to forum members who were absolutely sure they understood it).

    Long story short, CIQ 3.4.* was released concurrently with CIQ 4.2.*, at a time when both CIQ 3 and CIQ 4 devices were receiving new features. Collectively, these release were known as "System 6". [System 4 was CIQ 3.2 / 4.0, and System 5 was CIQ 3.3 / 4.1]

    System 6 support was introduced in SDK 4.2 (which was a beta). The next production SDK was 4.2.1.

    Looking at the changelog around that time, this change that was introduced in 3.2.1 seems relevant:

    > Update bitmap initializer and setBitmap() to allow passing a BitmapResource

    [3/x]

  • The API doc for SDK 3.2.1 does have a :bitmap option for Bitmap.initialize():

    Note that SDK 3.2.1 only applied to CIQ 3.* devices (CIQ 4 didn't exist at the time, and neither did system levels. At this time, the CIQ and SDK versions were still synced)

    I do think there's a bug in the setBitmap() doc here, as it still says that it only accepts a Symbol (resource ID)  when it should also accept a BitmapResource (as per the changelog.)

    The relevant part here is that internally, a Symbol is just an integer.

    Assuming this version of setBitmap() accepts either a Symbol (Number) or BitmapResource, and this is what's on the Fenix 6 Pro, that would explain the error message you are seeing.

    But wait, Fenix 6 Pro is on CIQ 3.4.5, not CIQ 3.2.1!

    Well, let's look at some later SDKs:

    - SDK 4.0.1 was the first production SDK to supported System 4: CIQ 3.2 / 4.0. It has the following API doc for Bitmap:

    But wait, this can't be right. We know that the Bitmap constructor calls setBitmap, yet according to the docs, the constructor only takes a resource ID or BitmapResource, but setBitmap takes a BitmapType, Symbol or Number? Doesn't make sense for the constructor to accept more Bitmap* types than setBitmap.

    Indeed the changelog for SDK 4.0.5 says that typing issues were fixed for Bitmap, which was reflected in the API doc:

    This is somewhat closer to the setBitmap() doc we all know and love, except for the seemingly superfluous Number type.

    What I think happened is that even though Bitmap.initialize() and Bitmap.setBitmap() were improved with SDK 4.*, not all of the changes made it to the CIQ 3.* devices. This is one of the problems with the System Level scheme (at the time when both CIQ 3 and CIQ 4 were supported). It wasn't always clear which features were on both CIQ 3 and 4 devices, and which were CIQ 4-only.

    Anyway this is the doc in the current 8.1.1 SDK:

    A lot has changed, but notably, setBitmap() no longer accepts a Number. I think it doesn't matter since I'm fairly sure the Number was only there because the resource ID was represented by a Symbol, and symbols are internally implemented as integers afaik.

    Anyway, based on all the anecdotal evidence, here's my guesses:

    1) I think that CIQ 3.* devices (even CIQ 3.4.*) never received the ability to initialize a Bitmap from anything other than a resource ID or BitmapResource. I'm basing this on your observed behaviour with Fenix 6 Pro (CIQ 3.4.*)

    2) I think that FR935 (CIQ 3.1.*) never got the ability to initialize a Bitmap from anything other than a resource ID.

    3) Neither 1 nor 2 is documented. The doc doesn't say how the available keys/values in the options dictionary differ based on CIQ

    4) Neither 1 nor 2 is reflected in the sim. For example, if I pass a BufferedBitmap into the bitmap constructor on FR935, it seems to work in the sim, but it clearly doesn't work on a real device. Based on the SDK docs alone, it should not work. (Nothing that was documented after SDK 3.1.* should be available on FR935.)

    Anyway this seems like a huge mess to me. If you decide to post a bug report, I'll upvote it.

    [4/x]

  • But on an actual device, the Bitmap constructor throws an UnexpectedTypeException. Apparently, it expects a real BufferedBitmap and not a subclass.

    Besides, this is obviously a catch-22. If the (Fenix 6 Pro) device expects a BufferedBitmap instance for Bitmap.initialize({ :bitmap => ... }), then obviously it can't work (since BufferedBitmap lacks getHeight / getWidth on that device).

    otoh, it's not at all reasonable for Garmin to design an API that only works when you pass in a subclass of the expected type which has some extra members (which are explicitly documented to not exist for that device).

    The simplest explanation (even without all of the "detective work" above) is that BufferedBitmap was never supported as an input to Bitmap.initialize or Bitmap.setBitmap, in CIQ 3.4.* (e.g. Fenix 6 Pro).

  • I went ahead and posted a bug report

    Thanks a lot! I agree — it's likely that BufferedBitmap wasn’t intended to be used as input for Bitmap prior to API level 4.0.0. Ideally, this should be reflected in the documentation and caught by the compiler. I'm not sure if there’s any precedent for this — are there other cases in the API where a function parameter type is only valid starting from a specific API level?