This week, I needed to use Flurry Analytics in my Xamarin.iOS and my Xamarin.Android apps, but there was no .NET SDK for these platforms. Flurry did provide a SDK for each platform, but it was the native libraries, which cannot be used directly in .NET. Given this, I decided to bind those native libraries using Xamarin.iOS and Xamarin.Android.
I have split this project up into four parts:
- Introduction and Pre-requisites
- Xamarin.iOS binding
- Xamarin.Android binding
- Flurry.Analytics in the wild
After binding the iOS SDK for Flurry Analytics, we are going to move onto the Android release. This task needs only one thing from the downloaded SDK:
- [Android-sdk-path]/Android 4.1.0/FlurryAnalytics/FlurryAnalytics-4.1.0.jar
Of course, the version numbers may change for later releases. The java archive file (.jar) is going to be used to generate the .NET interfaces and enums, and then be embedded in the resulting assembly.
Just before we do the real binding, we should just create our C# solution for the Xamarin.Android binding. Just create a new project/solution and select the Android “Bindings Library” template. For my project name I used
Flurry.Analytics.Android, but you could use anything.
The default project has a few files and directories in it, we will have a look at each one in depth as we go through the binding steps:
- /Additions/ (this allows you to add arbitrary C# to the generated classes
before they are compiled)
- /Properties/AssemblyInfo.cs (the usual assembly information attributes)
- /Jars/ (this directory is for Android .jars)
- /Transforms/EnumFields.xml (this allows you to map Java int constants to C# enums)
- /Transforms/EnumMethods.xml (this allows changes to method parameters and return types from Java int constants to C# enums)
- /Transforms/Metadata.xml (this allows changes to be made to the final API)
Creating the Initial Binding
This step will involve just adding the jar to the project, and letting the compiler do it’s thing, but we will almost always have to go in and tweak a few things in the
So, first things first, add the jar file to the project and compile. The build will probably fail with error messages containing single character methods and types. This is because the actual jar file has been obfuscated. However this can easily be fixed.
The way the generation works is that there is a two step process, there is a tool that generates a bunch of xml files from the jar file. These xml files are then used to generate C# code. The
Metadata.xml sits in between the two steps and can be used to transform the generated xml before the C# generation.
Tweaking the Generated Binding (Manifest.xml)
As we can see that the build fails on obfuscated types, we can remove these. The removal is safe as we are only preventing the .NET binding from being created, not actually removing the underlying Java members. And, as these items have been obfuscated, we can safely assume that we aren’t supposed to be accessing them anyway.
But, instead of removing each member or type that appears, we can use a great tool that will decompile the jar file and show us exactly what types are internal and what types we should be binding. I use JD-GUI, which is a free Java decompiler. They also have a nice online version, JD-Online. What we can do is to just upload the jar file here and see what’s inside:
As we can see, the impl and sdk branches contain internal and obfuscated types, so we can remove those:
<remove-node path="/api/package[starts-with(@name, 'com.flurry.sdk')]" />
<remove-node path="/api/package[starts-with(@name, 'com.flurry.android.impl')]" />
After this, the build should now succeed and we will have an assembly that we can use. However, there are still some types that we can remove to clean the API a bit:
InstallReciever is not used by the consumer, so this is safe to remove, but
Constants is still used. If we remove this, then the consumer will not have access to the values on the type. We can see that
Constants just contains the values representing male, female and unknown.
To remove the
InstallReciever, we can add this line to
<remove-node path="/api/package[@name='com.flurry.android']/class[@name='InstallReceiver']" />
Constants type, we will do something different.
Managing the Enumerations (Additions & EnumFields.xml)
As we clean up the API, we want to remove the
Constants type and replace it with an enum. One way to do this is to use the
<mapping clr-enum-type="Flurry.Analytics.Gender" jni-interface="com/flurry/android/Constants">
<field clr-name="Male" jni-name="MALE" value="1" />
<field clr-name="Female" jni-name="FEMALE" value="0" />
<field clr-name="Unknown" jni-name="UNKNOWN" value="-1" />
This will generate a nice enum for us, but this does leave an unused interface
IConstants. As this is a very small library, we can do this mapping slightly differently. First we remove the entire
Constants type in the
<remove-node path="/api/package[@name='com.flurry.android']/interface[@name='Constants']" />
Then, we can create the enum in the
/Additions/ directory. To do this, add a new file (for example called
FlurryAgent.cs) under this directory and add the enum:
public enum Gender
Male = 1,
Female = 0,
Unknown = -1
Now there is one last thing to do before the API definition is complete. There is a
SetGender method on the type
FlurryAgent, which takes the type
sbyte. It is not intuitive to use the
Gender enum here, so we can fix this in a two step process. First we will create an overload in the
FlurryAgent.cs file that accepts a
Gender enum member as an argument:
public partial class FlurryAgent
public static void SetGender(Gender gender)
And, what we can do is also hide the original member as the new overload is good enough:
<attr path="/api/package[@name='com.flurry.android']/class[@name='FlurryAgent']/method[@name='setGender']" name="visibility">internal</attr>
And with this, our binding is complete, although we can do a few nice changes to the namespace and parameter names.
Changing Parameter Names
Now that we have the binding complete, we can see that it is using the namespace
Com.Flurry.Android, which is no .NET-like at all. We can change this to something better:
<attr path="/api/package[@name='com.flurry.android']" name="managedName">Flurry.Analytics</attr>
This maps the
com.flurry.android package name to the neat
Flurry.Analytics namespace. One last thing is to fix the parameter names. Sometimes you can use the JavaDocs, but in this case, I couldn’t get it to work. Doing it manually is not hard, but it is boring and time consuming, but here are a few.
This is the usual mapping:
<attr path="/api/package[@name='com.flurry.android']/class[@name='FlurryAgent']/method[@name='onStartSession']/parameter[@name='p0']" name="name">context</attr>
<attr path="/api/package[@name='com.flurry.android']/class[@name='FlurryAgent']/method[@name='onStartSession']/parameter[@name='p1']" name="name">apiKey</attr>
<attr path="/api/package[@name='com.flurry.android']/class[@name='FlurryAgent']/method[@name='onEndSession']/parameter[@name='p0']" name="name">context</attr>
If there are complicated parameter types (note the
<attr path="/api/package[@name='com.flurry.android']/class[@name='FlurryAgent']/method[@name='logEvent']/parameter[@name='p1' and @type='java.util.Map<java.lang.String, java.lang.String>']" name="name">parameters</attr>
If there are overloads with different parameter types:
<attr path="/api/package[@name='com.flurry.android']/class[@name='FlurryAgent']/method[@name='onError']/parameter[@name='p2' and @type='java.lang.Throwable']" name="name">exception</attr>
<attr path="/api/package[@name='com.flurry.android']/class[@name='FlurryAgent']/method[@name='onError']/parameter[@name='p2' and @type='java.lang.String']" name="name">errorClass</attr>
After doing this for all the members, the binding is now complete.
So far we have defined our API, added any additional logic or types, added the native library and added parameter names. This is all that is needed, so our library should build fine now and we should be able to use it in an Xamarin.Android app:
// the methods
// the properties that we changed back into methods
string version = FlurryAgent.ReleaseVersion;
// the extra method that we added