Search code examples
android-layoutjava-native-interfacexamarin.android

Mono Android: Java Android custom view JNI not calling constructors in xml layout


We're using Mono for Android, and we want to use a few custom view subclasses that we have written in Java Android. We created a C# "bridge" class to expose the Java class through JNI. The method overrides and custom methods we exposed are working fine when called from C# through the C# bridge class, but when we use the class in an XML layout (by referencing the fully qualified java namespace for the view), it never seems to call the constructor through C#, so the C# type does not get set properly.

XML Layout

  <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent"
                  android:layout_height="fill_parent" >
        <com.example.widget.ImageView:id="@+id/custom_view" /> 
  </merge>

C# "Bridge" class constructors

namespace Example.Widgets {
  [Register ("com/example/widget/ImageView", DoNotGenerateAcw=true)]
  public class ImageView : global::Android.Widget.ImageView {
    private new static IntPtr class_ref = JNIEnv.FindClass("com/example/widget/ImageView");
    private static IntPtr id_ctor_Landroid_content_Context_;
    private static IntPtr id_ctor_Landroid_content_Context_Landroid_util_AttributeSet_;
    private static IntPtr id_ctor_Landroid_content_Context_Landroid_util_AttributeSet_I;
    ...

    protected override IntPtr ThresholdClass {
        get {
            return ImageView.class_ref;
        }
    }

    protected override Type ThresholdType {
        get {
            return typeof(ImageView);
        }
    }

    protected ImageView (IntPtr javaReference, JniHandleOwnership transfer) : base (javaReference, transfer) {
    }

    // --V-- this should get called by the android layout inflater, but it never does (nor do any of the other public constructors here)
    [Register (".ctor", "(Landroid/content/Context;Landroid/util/AttributeSet;)V", "")]
    public ImageView (Context context, IAttributeSet attrs) : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer)
    {
        Log.Debug("ImageView","Calling C# constructor (Landroid/content/Context;Landroid/util/AttributeSet;)V");
        if (base.Handle != IntPtr.Zero)
        {
            return;
        }
        if (base.GetType () != typeof(ImageView))
        {
            base.SetHandle (JNIEnv.CreateInstance (base.GetType (), "(Landroid/content/Context;Landroid/util/AttributeSet;)V", new JValue[]
            {
                new JValue (JNIEnv.ToJniHandle (context)),
                new JValue (JNIEnv.ToJniHandle (attrs))
            }), JniHandleOwnership.TransferLocalRef);
            return;
        }
        if (ImageView.id_ctor_Landroid_content_Context_Landroid_util_AttributeSet_ == IntPtr.Zero)
        {
            ImageView.id_ctor_Landroid_content_Context_Landroid_util_AttributeSet_ = JNIEnv.GetMethodID (ImageView.class_ref, "<init>", "(Landroid/content/Context;Landroid/util/AttributeSet;)V");
        }
        base.SetHandle (JNIEnv.NewObject (ImageView.class_ref, ImageView.id_ctor_Landroid_content_Context_Landroid_util_AttributeSet_, new JValue[]
        {
            new JValue (JNIEnv.ToJniHandle (context)),
            new JValue (JNIEnv.ToJniHandle (attrs))
        }), JniHandleOwnership.TransferLocalRef);
    }



    [Register (".ctor", "(Landroid/content/Context;)V", "")]
    public ImageView (Context context) : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer) {
      ...
    }


    [Register (".ctor", "(Landroid/content/Context;Landroid/util/AttributeSet;I)V", "")]
    public ImageView (Context context, IAttributeSet attrs, int defStyle) : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer)    {
      ...
    }

    ...

Java Class

  package com.example.widget;
  public class ImageView extends android.widget.ImageView {

      //--V-- This constructor DOES get called here (in java)
      public ImageView(Context context, AttributeSet attrs) {
          super(context, attrs);
          Log.d(TAG, "Called Java constructor (context, attrs)");
          ...
      }
  }

When we stop at a breakpoint in the program, we see that the local for our image view is of type Android.Widgets.ImageView, but holds the value {com.example.widget.ImageView@4057f5d8}. We're thinking that the type should show Example.Widgets.ImageView as well, but we can't figure out how to get .NET to use this type instead of the inherited Android type (Android.Widgets.ImageView).

Any ideas how to get .NET to call constructors properly when views exposed through JNI are used in XML layout?

Thanks in advance


Solution

  • The problem is that I haven't fully documented/explained what RegisterAttribute.DoNotGenerateAcw does, for which I must apologize.

    Specifically, the problem is this: Android Layout XML can only refer to Java types. Normally this isn't a problem, as Android Callable Wrappers are generated at build-time, providing Java types for every C# type which subclasses Java.Lang.Object.

    However, Android Callable Wrappers are special: they contain Java native method declarations, and Android Callable Wrapper constructors call into the Mono for Android runtime to create the relevant C# class. (See the example at the above url, and notice that the constructor body calls mono.android.TypeManager.Activate().)

    Your example, however, completely bypasses all of that, because your layout XML isn't referencing an Android Callable Wrapper, it's instead referencing your Java type, and your Java type doesn't have a constructor that calls mono.android.TypeManager.Activate().

    The result is that happens is exactly what you told it to happen: Your Java type is instantiated, and because there is no "plumbing" to associate the Java instance with a (to be) created C# instance (the mono.android.TypeManager.Activate() call), no C# instance is created, which is the behavior you're seeing.

    All of which sounds entirely sane to me, but I'm the guy who wrote it, so I'm biased.

    Thus, what do you want to have happen? If you really want a hand-written Java ImageView, as you've done, then there's only one reasonable C# constructor to have invoked: the (IntPtr, JniHandleOwnership) constructor. All that's missing is the mapping between the Java type and the C# type, which you can do "somewhere" during app startup via TypeManager.RegisterType():

    Android.Runtime.TypeManager("com/example/widget/ImageView",
            typeof(Example.Widgets.ImageView));
    

    If you have that mapping in place, then when the com.example.widget.ImageView instance is surfaced in managed code, it will be wrapped with an Example.Widgets.ImageView instance, using the (IntPtr, JniHandleOwnership) constructor.

    If, instead, you want to have your C# (Context context, IAttributeSet attrs, int defStyle) constructor invoked, you should bypass your wrapper type and just stick to C# code:

    namespace Example.Widgets {
        public class ImageView : global::Android.Widget.ImageView {
            ...
    

    In summary, RegisterAttribute.DoNotGenerateAcw is "special": it means that you're "aliasing" an existing Java type, and that the "normal" Android Callable Wrapper generation should be skipped. This allows things to work (you can't have two different types with the same fully qualified name), but adds a different set of complications.