Following the Android Toasts Tutorial from a JNI Perspective

This article needs a technical review. How you can help.

This article is a work in progress and is based on GitHubGIST :: _ff-addon-tutorial-jniAndroidToast.js

This article will follow the Android Developers :: API Guides - Toasts tutorial. It will use the JNI.jsm module that is available in Firefox for Android since version 17. This article teaches developers how to port Java code to JNI by reading the Java and Android documentation.

Toast description

A toast provides simple feedback about an operation in a small popup. It only fills the amount of space required for the message, and the current activity remains visible and interactive. For example, navigating away from an email before you send it triggers a "Draft saved" toast to let you know that you can continue editing later. Toasts automatically disappear after a timeout.

Toasts do not have to be created with JNI; they are exposed by the NativeWindow feature on Firefox for Android, see toast documentation.

Java code

Let's start with the following Java code, which invokes a toast and says "Hello, Firefox!".

Context context = getApplicationContext();
CharSequence text = "Hello, Firefox!";
int duration = Toast.LENGTH_SHORT;

Toast toast = Toast.makeText(context, text, duration);
toast.show();

NativeWindow code

As mentioned earlier, toasts are a very popular feature, so Mozilla developers chose to bring it to the privileged JavaScript scope via the NativeWindow object. The Java code example above can be done with privileged JavaScript from Firefox for Android with the following code:

window.NativeWindow.toast.show("Hello, Firefox!", "short"); 

Converting Java to JNI.jsm

The first step is to look at the Java code and see all the different types, methods, constructors, and fields that are being used. In the toast code we have the following:

  • Types - Context, CharSequence, int, Toast, and void
  • Methods - makeText, show
  • Fields - LENGTH_SHORT

No constructors are used.

Declare the types

We will start by declaring the types needed. In JNI, there is the all-important property called sig. When something is declared, we are stating the sig and the name. The sig's will be represented as JavaScript strings in JNI.jsm, and the sig's of the types are as follows:

Java Type Signature
boolean Z
byte B
char C
class/object Lclass name in slash notation here;
double D
float F
int I
long J
short S
void V
array of type [sig here
method format (sig of type of each argument here)sig of the return type here

Declaring a class/object is done in a special way. For example, if the class is blah.foo.bar then the signature of this will be this in slash notation with an L and ; around it. So this would be Lblah/foo/bar;. If there is a sub class such as blah.foo.bar.subclass then a dollar sign is used in the slash notation, so this would be Lblah/foo/bar$subclass;.

When declaring methods, the signatures of the types go right next to each other without any spacing. The arguments are within the parenthesis, and the return type is immediately following. More on this later.

So let us now declare the types for our toast example. We see we have three classes and two basic types (int and void). The class signature is looked up on the Android documentation. For instance, we go to Android Reference :: Context and we see:

How to look up the signature of a class/object on Android documentation.

We see that the signature in dot notation is android.content.Context. So this will be written as Landroid/content/Context;. For convenience, let's make an object called SIG and populate it with our types.

let SIG = {
    Context: 'Landroid/content/Context;',
    CharSequence; 'Ljava/lang/CharSequence;',
    int: 'I',
    Toast: 'Landroid/widget/Toast;',
    void: 'V',
};

Get Java environment

Before we go on to declare the other important features, we need to load our environment variable. We want to wrap this in a try-catch statement so if any errors occur we make sure to unload the classes and the Java environment, this is good for performance.

This is the template that will follow our object of signatures:

var my_jenv = null;
try {
    my_jenv = JNI.GetForThread();

    // do the jni work here

} finally {
    if (my_jenv) {
        JNI.UnloadClasses(my_jenv);
    }
}

The reason we choose my_jenv for a variable name, and not jenv, is because the global privileged window scope of Firefox for Android has a variable jenv already, and we don't want to mix.

Declare constructors, methods, and fields

Now that the sig's and Java environment is ready, it is now possible to declare the constructors, methods, and fields.

Load the class

The first step is to load the class. Every constructor, method, and field belongs to a class. In our case, we have a class called Toast, and to this belongs makeText, show, and LENGTH_SHORT. To load the class the following is done:

let Toast = JNI.LoadClass(my_jenv, SIG.Toast.substr(1, SIG.Toast.length - 2), {
    // declares of constructors, static_methods, methods, fields, and static_fields go here
});

The variable name of Toast was used, but it does not matter, you can use whatever you want.

The first argument to LoadClass is the Java environment variable, my_jenv.

The second argument is the class sig without the surrounding L and ;. Because we don't want to keep typing string signatures everywhere, we can end up with typos and hard to trace back errors, we use the SIG object. We see SIG.Toast.substr(1, SIG.Toast.length - 2) was written, this can be replaced with 'android/widget/Toast'.

The third argument is an object of our declares.

Declare constructors

In our Toast example, we do not have any constructors.

Determine static-ness of methods declare them

There are two types of methods, chainable and static. If it is static, this means that it can be called directly from the class. If it is not static, then it cannot be used on an instance of that class.

How is it determined if a method is static or not? The documentation tells us. For example, in our case we have a method makeText and show, looking that up on the Android documentation page shows this:

This is how to read the documentation to see if it is static. We see before the return type is the word "static" this means it is a static and cannot be chained on an instance, it must be chained after the class only.

The documentation does not have the word "static" in front of the return type, indicating this is a chainable.

There are two columns, the first column is the return, and the second column is the method name and arguments. We see that makeText returns static Toast. This means it is a static method. We see that the return column of the show method does not contain the word static. Therefore, it is a plain method. Let's declare these methods in the third argument of LoadClass we add a key-value pair, with the key being static_methods and methods and the value being an array.

var Toast = JNI.LoadClass(my_jenv, SIG.Toast.substr(1, SIG.Toast.length - 2), {
    static_methods: [
        { name: 'makeText', sig: '()' }
    ],
    methods: [
        { name: 'show', sig: '()' }
    ]
});

The sig of methods is always in the format of "method format" from our types table above. The reason is that methods accept arguments and return something.

We need to declare now the arguments and return type for each of the methods. The documentation says makeText takes three arguments, the types are Context, int, and int and returns a Toast. For example, let's declare this with strings:

{ name: 'makeText', sig: '(Landroid/content/Context;II)Landroid/widget/Toast;' }

We see there are no separating characters between the arguments. Because we want to avoid typos, we will avoid typing the signatures and user our SIG object.

{ name: 'makeText', sig:
    '(' + 
        SIG.Context +    // context
        SIG.int +        // resId
        SIG.int +        // duration
    ')' +
    SIG.Toast            // return
}

Let's do the same for show and here are our methods placed into our third argument of LoadClass:

static_methods: [
    { name: 'makeText', sig:
        '(' + 
            SIG.Context +    // context
            SIG.int +        // resId
            SIG.int +        // duration
        ')' +
        SIG.Toast            // return
    }
],
methods: [
    { name: 'show', sig:
        '()' +        // no arguments
        SIG.void    // return
    }
]

Determine static-ness of fields declare them

Just like methods, fields have two types, static and not. We look up the documentation for our field, which is LENGTH_SHORT, and we see it is static. So we will not need a group in the third argument of LoadClass for fields, we will only need static_fields. Fields do not accept arguments, so the "method format" is not used, we simply tell the sig the type of the field, which we find out from the Android documentation website.

This is what our fields array will look like:

static_fields: [
    { name: 'LENGTH_SHORT', sig: SIG.int }
]

Wrap it all up

Here is the code for our completed Toast class:

var Toast = JNI.LoadClass(my_jenv, SIG.Toast.substr(1, SIG.Toast.length - 2), {
    static_methods: [
        { name: 'makeText', sig:
            '(' + 
                SIG.Context +    // context
                SIG.int +        // resId
                SIG.int +        // duration
            ')' +
            SIG.Toast            // return
        }
    ],
    methods: [
        { name: 'show', sig:
            '()' +        // no arguments
            SIG.void    // return
        }
    ],
    static_fields: [
        { name: 'LENGTH_SHORT', sig: SIG.int }
    ]
});

getApplicationContext()

Many JNI codes need an entry point, sometimes it is the context, sometimes it is the activity. In our Toasts example, we need the context. Our application is Firefox for Android, so we need to tap into the Firefox classes to get its context. We search DXR, and we find a getContext static method belonging to the GeckoAppShell class.

To figure out the sig for Firefox classes, we have to search on DXR. We find an XML file showing the sig for GeckoAppShell is org.mozilla.gecko.GeckoAppShell. We will add this to our SIG object in slash notation:

GeckoAppShell: 'Lorg/mozilla/gecko/GeckoAppShell;'

We then instantiate the GeckoAppShell class in JNI and declare the getContext method; DXR showed that this method accepted no arguments and returns a Context, and we already had the sig for Context in our SIG object.

var geckoAppShell = JNI.LoadClass(my_jenv, SIG.GeckoAppShell.substr(1, SIG.GeckoAppShell.length - 2), {
    static_methods: [
        { name: 'getContext', sig: '()' + SIG.Context }
    ]
});

Porting the Java code

Now that all the declarations are complete, we can now side-by-side port the Java code to JNI.

// Context context = getApplicationContext();
var context = geckoAppShell.getContext();

// CharSequence text = 'Hello toast!';
var text = 'Hello toast!';

// int duration = Toast.LENGTH_SHORT;
var duration = Toast.LENGTH_SHORT;

// Toast toast = Toast.makeText(context, text, duration);
var toast = Toast.makeText(context, text, duration);

// toast.show();
toast.show();

Notice that the CharSequence was not created with a constructor. This is special: Because a CharSequence is an array of byte characters, we can pass simple JavaScript strings to JNI.jsm wherever a CharSequence is needed. This tells us we don't even need the sig for CharSequence in our SIG object, so let's remove it.

Final JNI ported code

Here's converted code, which works with copy-ann-paste into Scratchpad, with "Main Process" as the target.

var my_jenv = null;
try {
    my_jenv = JNI.GetForThread();

    // We declare the classes, methods, etc we will be using: this part is not seen in the native example
    var SIG = {
        Context: 'Landroid/content/Context;',
        GeckoAppShell: 'Lorg/mozilla/gecko/GeckoAppShell;',
        int: 'I',
        Toast: 'Landroid/widget/Toast;',
        void: 'V'
    };

    var Toast = JNI.LoadClass(my_jenv, SIG.Toast.substr(1, SIG.Toast.length - 2), {
        static_methods: [
            { name: 'makeText', sig:
                '(' + 
                    SIG.Context +    // context
                    SIG.int +        // resId
                    SIG.int +        // duration
                ')' +
                SIG.Toast            // return
            }
        ],
        methods: [
            { name: 'show', sig:
                '()' +        // no arguments
                SIG.void    // return
            }
        ]
        static_fields: [
            { name: 'LENGTH_SHORT', sig: SIG.int }
        ]
    });

    var geckoAppShell = JNI.LoadClass(my_jenv, SIG.GeckoAppShell.substr(1, SIG.GeckoAppShell.length - 2), {
        static_methods: [
            { name: 'getContext', sig: '()' + SIG.Context }
        ]
    });

    // ok this ends the JNI declare stuff, now below you will see native-by-jni side-by-side

    // Context context = getApplicationContext();
    var context = geckoAppShell.getContext();

    // CharSequence text = 'Hello toast!';
    var text = 'Hello toast!';

    // int duration = Toast.LENGTH_SHORT;
    var duration = Toast.LENGTH_SHORT;

    // Toast toast = Toast.makeText(context, text, duration);
    var toast = Toast.makeText(context, text, duration);

    // toast.show();
    toast.show();

} finally {
    if (my_jenv) {
        JNI.UnloadClasses(my_jenv);
    }
}

References

See also

  • JNI.jsm - How to import and use the JNI functions used in this article

Document Tags and Contributors

 Contributors to this page: rolfedh, arai, Noitidart
 Last updated by: rolfedh,