Issue binding AAR containing Enums (Spotify Auth SDK)

BUBBUB AUMember
edited January 2017 in Xamarin.Android

Hi all,

I'm new to Xamarin (and a novice for C#, JAVA and Android - C++ developer by trade) and I am struggling to produce correct bindings for third party Spotify Authentication SDK to be used my app. The issue relates to broken auto-generated binding code and me struggling to correctly map the Enumeration fields and methods (or whether I even need to do this in the first place).

Raw output from AAR binding generated code

Adding the AAR built from the SDK above and setting the Build Action to LibraryProjectZip produces the following broken binding:

        internal static IntPtr class_ref {
            get {
                return JNIEnv.FindClass ("com/spotify/sdk/android/authentication/AuthenticationResponse", ref java_class_handle);
            }
        }

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

        protected override global::System.Type ThresholdType {
            get { return typeof (AuthenticationResponse); }
        }

        static IntPtr id_getType;
        // Metadata.xml XPath method reference: path="/api/package[@name='com.spotify.sdk.android.authentication']/class[@name='AuthenticationResponse']/method[@name='getType' and count(parameter)=0]"
        [Register ("getType", "()Lcom/spotify/sdk/android/authentication/AuthenticationResponse$Type;", "GetGetTypeHandler")]
        public virtual unsafe global::Com.Spotify.Sdk.Android.Authentication.AuthenticationResponse.Type GetType ()
        {
            if (id_getType == IntPtr.Zero)
                id_getType = JNIEnv.GetMethodID (class_ref, "getType", "()Lcom/spotify/sdk/android/authentication/AuthenticationResponse$Type;");
            try {

                if (GetType () == ThresholdType)
                    return global::Java.Lang.Object.GetObject<global::Com.Spotify.Sdk.Android.Authentication.AuthenticationResponse.Type> (JNIEnv.CallObjectMethod (((global::Java.Lang.Object) this).Handle, id_getType), JniHandleOwnership.TransferLocalRef);
                else
                    return global::Java.Lang.Object.GetObject<global::Com.Spotify.Sdk.Android.Authentication.AuthenticationResponse.Type> (JNIEnv.CallNonvirtualObjectMethod (((global::Java.Lang.Object) this).Handle, ThresholdClass, JNIEnv.GetMethodID (ThresholdClass, "getType", "()Lcom/spotify/sdk/android/authentication/AuthenticationResponse$Type;")), JniHandleOwnership.TransferLocalRef);
            } finally {
            }
        }

In particular 'if (GetType () == ThresholdType)' gives the error Operator '==' cannot be applied to operands of type 'AuthenticationResponse.Type' and 'Type'. This appears multiple times throughout the code and the definition of GetType() and ThreshholdType are included in the snippets above. Simple enough, I can fix this by hand by casting GetType as such '((object)this).GetType ()' which will compile and run without issue until the next call 'return JNIEnv.GetString (JNIEnv.CallObjectMethod (((global::Java.Lang.Object) this).Handle, id_getAccessToken), JniHandleOwnership.TransferLocalRef);' which fails within the dll.

Output from AAR binding generated code with Enum definition attempts

The fact that the auto-generated code above was incorrect indicated that the methods I am trying to bind here may not be binding correctly. I did some reading on Enum binding definitions and came up with the Metadata.xml, EnumFields.xml and EnumMethods.xml (attached due to formatting difficulty) respectively. For reference the spotify library I am trying to bind contains three methods to the Com.Spotify.Sdk.Android.Authentication.AuthenticationResponse.Type enumeration.

Enum definition:
package com.spotify.sdk.android.authentication;

public class AuthenticationResponse implements Parcelable {

    public enum Type {
        CODE("code"),
        TOKEN("token"),
        ERROR("error"),
        EMPTY("empty"),
        UNKNOWN("unknown");

        private final String mType;

        Type(String type) {
            mType = type;
        }

        @Override
        public String toString() {
            return mType;
        }
    }

Enum references:
com.spotify.sdk.android.authentication.AuthenticationRequest.Builder

Builder(String clientId, AuthenticationResponse.Type responseType, String redirectUri) 

com.spotify.sdk.android.authentication.AuthenticationResponse.Builder

public AuthenticationResponse.Builder setType(AuthenticationResponse.Type type)

com.spotify.sdk.android.authentication.AuthenticationResponse

public AuthenticationResponse.Type getType()

Note the remove-node for the AuthenticationResponse class in Metadata.xml as adding in the Enum methods and fields produced a declaration duplication of AuthenticationResponse in the Com.Spotify.Sdk.Authentication namespace.

The result of this mapping produces the single error 'Cannot convert from 'Com.Spotify.Sdk.Authentication.AuthenticationResponse.Type' to 'bool' for the line '__args [1] = new JValue (p1);' below.

                  static IntPtr id_ctor_Ljava_lang_String_Lcom_spotify_sdk_android_authentication_AuthenticationResponse_Type_Ljava_lang_String_;
                  // Metadata.xml XPath constructor reference: path="/api/package[@name='com.spotify.sdk.android.authentication']/class[@name='AuthenticationRequest.Builder']/constructor[@name='AuthenticationRequest.Builder' and count(parameter)=3 and parameter[1][@type='java.lang.String'] and parameter[2][@type='com.spotify.sdk.android.authentication.AuthenticationResponse.Type'] and parameter[3][@type='java.lang.String']]"
                  [Register (".ctor", "(Ljava/lang/String;Lcom/spotify/sdk/android/authentication/AuthenticationResponse$Type;Ljava/lang/String;)V", "")]
                  public unsafe Builder (string p0, global::Com.Spotify.Sdk.Android.Authentication.AuthenticationResponse.Type p1, string p2)
                      : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer)
                  {
                      if (((global::Java.Lang.Object) this).Handle != IntPtr.Zero)
                          return;
                      IntPtr native_p0 = JNIEnv.NewString (p0);
                      IntPtr native_p2 = JNIEnv.NewString (p2);
                      try {
                          JValue* __args = stackalloc JValue [3];
                          __args [0] = new JValue (native_p0);
                          __args [1] = new JValue (p1);
                          __args [2] = new JValue (native_p2);
                          if (GetType () != typeof (Builder)) {
                              SetHandle (
                                      global::Android.Runtime.JNIEnv.StartCreateInstance (GetType (), "(Ljava/lang/String;Lcom/spotify/sdk/android/authentication/AuthenticationResponse$Type;Ljava/lang/String;)V", __args),
                                      JniHandleOwnership.TransferLocalRef);
                              global::Android.Runtime.JNIEnv.FinishCreateInstance (((global::Java.Lang.Object) this).Handle, "(Ljava/lang/String;Lcom/spotify/sdk/android/authentication/AuthenticationResponse$Type;Ljava/lang/String;)V", __args);
                              return;
                          }

Given the information above am I on the right track? Is mapping the enumeration above the correct route to travel down? The ((object)this).GetType() fix for the raw auto-generated code above doesn't feel correct - this file will constantly regenerate and need the fix applied on each build of the binding library.

Is it likely the run-time failure in the binding dll produced with the ((object)this).GetType() fix be failing due to a bad binding? The spotify SDK was built using their supplied script and I used the same JDK version used to produce this binding library.

Are my enumeration definitions correct or have I misunderstood the use case?

Apologies for the long winded post.

Best Answer

  • AlexRisnerAlexRisner US
    edited February 2017 Accepted Answer

    I actually was having the same issue in my project. GetType is actually a method in C#'s base Object class.

    So, just add this to your Metadata.xml to correct the bindings:
    <attr path="/api/package[@name='com.spotify.sdk.android.authentication']/class[@name='AuthenticationResponse']/method[@name='getType']" name="managedName">GetResponseType</attr>

    Seems to be working fine for me now. That should solve your issue.

Answers

  • BUBBUB AUMember

    I forgot to mention above that adding the enum mapping does not fix the incorrect generation issue causing 'if (GetType () == ThresholdType)' not to compile with error Operator '==' cannot be applied to operands of type 'AuthenticationResponse.Type' and 'Type'.

  • AlexRisnerAlexRisner USMember
    edited February 2017 Accepted Answer

    I actually was having the same issue in my project. GetType is actually a method in C#'s base Object class.

    So, just add this to your Metadata.xml to correct the bindings:
    <attr path="/api/package[@name='com.spotify.sdk.android.authentication']/class[@name='AuthenticationResponse']/method[@name='getType']" name="managedName">GetResponseType</attr>

    Seems to be working fine for me now. That should solve your issue.

  • BUBBUB AUMember

    @AlexRisner said:
    I actually was having the same issue in my project. GetType is actually a method in C#'s base Object class.

    So, just add this to your Metadata.xml to correct the bindings:
    <attr path="/api/package[@name='com.spotify.sdk.android.authentication']/class[@name='AuthenticationResponse']/method[@name='getType']" name="managedName">GetResponseType</attr>

    Seems to be working fine for me now. That should solve your issue.

    That worked perfectly - hard to believe it was that simple. Thanks for the help!

  • AlexRisnerAlexRisner USMember

    It's also worth mentioning that you may run into some compilation issues when you include the bindings in a project. It will complain about AuthenticationClient.AuthenticationClientListener is not public so it cannot be accessed outside of its package. For that, you'll need to add another line to your Metadata.xml:
    <remove-node path="/api/package[@name='com.spotify.sdk.android.authentication']/interface[@name='AuthenticationClient.AuthenticationClientListener']" />

    It will remove that interface from the bindings, but you're not really supposed to access that interface anyways. Everything should compile correctly after that change.

  • BUBBUB AUMember

    @AlexRisner said:
    It's also worth mentioning that you may run into some compilation issues when you include the bindings in a project. It will complain about AuthenticationClient.AuthenticationClientListener is not public so it cannot be accessed outside of its package. For that, you'll need to add another line to your Metadata.xml:
    <remove-node path="/api/package[@name='com.spotify.sdk.android.authentication']/interface[@name='AuthenticationClient.AuthenticationClientListener']" />

    It will remove that interface from the bindings, but you're not really supposed to access that interface anyways. Everything should compile correctly after that change.

    Thanks! This one I managed to figure out myself but it helps to know I've gone down the right track!

Sign In or Register to comment.