Android Plugins part 3: S3DXAndroidTools and native Android calls – ShiVa Engine

Android Plugins part 3: S3DXAndroidTools and native Android calls

In part 3 of the Android Plugin tutorial series, we are going to have a closer look at S3DXAndroidTools.jar, a ShiVa library which makes it easier to communicate with the Android API. With the help of this JAR, we are going to show you how to create a Toast, native message boxes, and set up a message pump to improve stability.

Project setup

We are going to use a very similar project setup as in the earlier tutorials, although we will refine it a bit. In ShiVa, we will start with the familiar Fire scene and the screen logging HUD. Taps are going to be registered through onMouseButtonDown, which will advance the test counter _nTest() and run through a number of test functions, which are defined in the new plugin we are going to write.

We are going to make a design change for the plugin right at the beginning. Since we know that this plugins is only ever going to work on Android since it relies on the Android API, we will not bother with setting up code for Linux, Mac or Windows plugins in the Platforms directory and instead make use of #ifdef __ANDROID__ heavily in the primary plugin file apjt.cpp itself. The boilerplate for determining the JNI environment now looks like this:

//apjt.cpp line 15
#ifdef __ANDROID__
    #include 

    static JNIEnv * apjt_GetJNIEnv() {
        JNIEnv * pJNIEnv;
        if ( PlugtestAndroJARTools::GetInstance()->pJavaVM && ( ((JavaVM*)(PlugtestAndroJARTools::GetInstance()->pJavaVM))->GetEnv((void**) &pJNIEnv, JNI_VERSION_1_4 ) >= 0 ) ) {
            return pJNIEnv;
        }
        return nullptr;
    }

    #include "Platforms/Android/cjartools.h"
    auto * _pc = new CJARTools();
#endif

Note how the definition for _pc has moved inside the #ifdef block and how there are no other Platforms/ includes. If a function uses Android-specific code that requires the _pc class pointer, we must wrap this call like so:

// apjt.cpp line 76
    #ifdef __ANDROID__
        _pc->makeToast(sMessage.GetStringValue());
    #else
        S3DX::log.warning("JARTools: Toast requires Android");
    #endif

This way, the plugin will still compile on non-android platforms and give you helpful warnings.

We also want to use a dedicated ShiVa AI module for Java communication. To make the code more flexible, we will need to modify our init() function to include the strings for the communication AI name as well as the default event where messages will be sent to. Our call is once again wrapped inside an #ifdef block:

// apjt.cpp line 48
    #ifdef __ANDROID__
        JNIEnv * p = apjt_GetJNIEnv();
        bOK.SetBooleanValue(_pc->init(p, sDefaultTargetAI.GetStringValue(), sDefaultTargetHandler.GetStringValue()));
    #else
        S3DX::log.warning("JARTools: Init requires Android");
        bOK.SetBooleanValue(false);
    #endif

Creating a JAR library with external dependencies

We will use Android Studio again to handle the Java portion of the plugin. Just like last time, we will create a new project (com.shivatech.java2) with a separate module inside (package com.shivatech.jartools), which is a Java JAR library:

Inside this package, we will add a new class called Java2. By default, this package has no idea of what Android is, nor what the Android ShiVa Tools are, so we have to add those libraries as external dependencies. The relevant dialogue box is hidden under File > Project Structure:

Click on Dependencies and then on your module to highlight all dependencies.

To add more, click on the plus icon under Declared dependencies and select Jar dependency:

You will need to add two packages. The first one is Android itself, which is located in the Android SDK folder structure. Choose the android.jar which matches your build target. The second library are the ShiVa tools, which you can find in the S3DX folder under Data/Authoring/Android in the ShiVa install directory. Change the configuration from implementation to api. It should look something like this:

Now you are ready to use both Android and ShiVaTool APIs in your Java module. Of course you must still declare every import you intend to use. For this tutorial, we are going to use Toasts and a simple dialogue box:

// Java2.java line 7
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.widget.Toast;

The ShiVa Android tools are declared like so:

// Java2.java line 13
import com.stonetrip.android.tools.*;

Android Studio should now recognize all these packages and give you proper code prediction, syntax highlighting and javadoc. If you type in S3DXAndroidTools. For instance, you should see this list of available API calls:

Making Toast

Toasts on Android are simple native message popups which require very little code and are therefor perfect for a first test of our build chain. All you need is a call to Toast.makeText which requires our ShiVa activity as first argument. Thankfully, the S3DXAndroidTools can help us here:

// Java2.java line 22
    public static void jToast(final String msg) {
        final Activity act = S3DXAndroidTools.getMainActivity();
        Toast.makeText(act, msg, Toast.LENGTH_LONG).show();
    }

Unfortunately, this code will crash your application. It turns out that you actually have to use proper Java threading by creating a new Runnable() object with a run() method override:

// Java2.java line 28
    public static void jToastThreaded(final String msg) {
        final Activity act = S3DXAndroidTools.getMainActivity();
        try {
            act.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(act, msg, Toast.LENGTH_LONG).show();
                }
            });
        } catch (Exception e) {
            apjt_msgDefault(1.f, "Exception on Toast thread: " +e.getMessage());
        }
    }

If you did everything correctly, your toast should look something like this:

Asking a simple (native) question

Our next test will create a simple yes/no message box, where the result will be captured and sent back to ShiVa. To keep things simple, we will AlertDialog.Builder() create the message box for us. Once again, threading is needed:

// Java2.java line 63
    public static void jDialogYN (final String question) {
        final Activity act = S3DXAndroidTools.getMainActivity();
        try {
            act.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    AlertDialog.Builder b = new AlertDialog.Builder(act);
                    b.setMessage(question).setPositiveButton("Yep", diaCall).setNegativeButton("Nope", diaCall).show();
                }
            });
        } catch (Exception e) {
            apjt_msgDefault(1.f, "Exception on DialogBuilder thread: " +e.getMessage());
        }
    }

Responses are handled by the diaCall callback:

// Java2.java line 46
    static DialogInterface.OnClickListener diaCall = new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            switch (which){
                case DialogInterface.BUTTON_POSITIVE:
                    apjt_msgDefault(0.f, "YES clicked");
                    break;

                case DialogInterface.BUTTON_NEGATIVE:
                    apjt_msgDefault(0.f, "NO clicked");
                    break;
            }
        }
    };

Depending on which button was clicked, the callback calls the bridge function apjt_msgDefault(), which is defined in both Java and C++, with a custom message:

// Java2.java line 83
private static native void apjt_msgDefault(final float kType, final String sMsg);

// cjartools.cpp line 21
static void apjt_msgDefault (JNIEnv * jE, jclass jC, const float nType, const jstring sMsg)

If you did everything correctly, your message box should look something like this:

The message pump

Unfortunately, returning your message box results to ShiVa is not as simple as I made you believe in the previous tutorial. This code will crash and leak:

static void apjt_msgDefault (JNIEnv * jE, jclass jC, const float nType, const jstring sMsg) {
    const char * cMsg   = jE->GetStringUTFChars(sMsg, 0);
    S3DX::user.sendEvent(S3DX::application.getCurrentUser(), CJARTools::getDefaultAI(), CJARTools::getDefaultEvent(), cMsg);
}

Even worse, the code will crash only sometimes, so you might no even catch it in testing. The problem here is that you are calling an S3DX:: function outside of a valid S3DX context, which for instance would be a plugin function that you have declared with the Plugin module in the ShiVa editor.

One possible solution to this issue is a message pump. The idea is simple: Instead of calling sendEvent() from a static function as soon as a Java event occurs, you collect them all in a queue-like structure. This queue is then emptied in a ShiVa context through a loop() plugin function which is called by onEnterFrame().

First, we need to define a data structure that holds our messages:

// cjartools.h line 10
typedef struct s_apjtMsg {
    float msgType;
    const char * ai;
    const char * event;
    const char * msg;
} apjtMsg;

These messages need to be stored in a dynamic data structure. I chose a std::vector for speed and simplicity:

// cjartools.h line 36
static std::vector _vMsg;

Message objects can easily be added to the vector through push_back() as soon as they are created. Instead of calling user.sendEvent(), our modified apjt_msgDefault() function now looks like this:

//cjartools.cpp line 21
static void apjt_msgDefault (JNIEnv * jE, jclass jC, const float nType, const jstring sMsg) {
    apjtMsg tmp = {nType, CJARTools::getDefaultAI(), CJARTools::getDefaultEvent(), jE->GetStringUTFChars(sMsg, 0)};
    CJARTools::_vMsg.push_back(tmp);
}

ShiVa needs to go through the _vMsg vector, read the messages and clear the old messages out. Here is a simple way to do this:

// cjartools.cpp line 111
void CJARTools::loop() {
    for (auto & i : _vMsg) {
        S3DX::user.sendEvent(S3DX::application.getCurrentUser(), i.ai, i.event, i.msgType, i.msg);
    }
    _vMsg.clear();
}

If you are using a std::vector like me and get compilation errors regarding strtof(), you need to comment out the overrides in the S3DX header files inside your plugin project:

// S3DXPlatform.h line 28
#elif (defined ANDROID_NDK)
    /* #   if (defined __clang__)
        extern      float   strtof  ( const char *, char ** ) ;
    #   else
        extern "C"  float   strtof  ( const char *, char ** ) __THROW ;
    #   endif */
    extern "C"          double  strtod  ( const char *, char ** ) ;
    #define                     S3DX_STRTOF  (float)strtod

The last thing to do is call your new loop() function from within your dedicated Java message AI onEnterFrame() handler:

If you did everything correctly, ShiVa will be able to receive your messages without crashing:

Please keep in mind that your vector is effectively a buffer. If you click on the message box faster than the pump can handle it, you will receive several messages in a single frame, and you no longer have a direct association between the cause (tap on screen) and effect (message sent):