This example shows how to construct a link between the Android Java implementation and cross-platform Kanzi code running on Android’s native side. Building a large scale JNI bridge manually can be time consuming, error prone, and hard to maintain. Here you can find instructions on how to expose your native Kanzi implementation on the Java side using SWIG.
You can find the example in <KanziWorkspace>/Examples/Android_SWIG.
SWIG is a tool you can use to generate JNI code and Java wrappers for native-side C++ implementation. Using generated Java files and polymorphism you can implement cross-platform parts on the native side and Android specific parts in Java. See SWIG.
Note: Modify SWIG_PATH in <KanziWorkspace>/Examples/Android_SWIG/Application/configs/platforms/android/swig/run_swig.bat and run_swig.sh to point to your SWIG installation path.
SWIG can generate Java wrappers for classes implemented in C++. To connect Java to the native Kanzi code, you have to implement a C++ interface. To implement part of the logic in Java you can override inherit the generated wrapper class and override virtual functions.
A common approach is to have an application wide singleton visible on the native and Java side. It typically has getters and setters for data providers and accessors for system features.
shared_data.hpp:
#ifndef SHARED_DATA
#define SHARED_DATA
class SharedData
{
public:
// Cross-platform accessor for shared data.
static SharedData& get();
// ...
protected:
SharedData();
// ...
};
#endif
shared_data.cpp:
#include "shared_data.hpp"
SharedData& SharedData::get()
{
static SharedData sharedData;
return sharedData;
}
SharedData::SharedData()
{
}
// ...
The above C++ class is a singleton you can use to share objects between the native implementation and Java. To expose it on the Java side you have to setup SWIG for the Kanzi project.
To enable SWIG for a Kanzi project:
// These instructions expose the SharedData singleton.
// The first line declares the module (Android_SWIG is the application name) and set the options.
// To enable polymorphism, you need the “directors” flag.
%module(directors="1") Android_SWIG
// You need %feature("director") <ClassName>; for each class where you want to override virtual functions.
%feature("director") SharedDataListener;
%feature("director") AndroidSWIGListener;
// General declarations
typedef bool kzBool;
%{
#define true KZ_TRUE
#define false KZ_FALSE
%}
#pragma SWIG nowarn=302 // Warns about redefinition of kzBool
// Includes for the wrapper C++ source file.
// To generate the wrapper for a C++ file add:
// - #include line to be copied to the generated JNI code
// - %include instruction to generate wrappers from it
// The paths for %include are relative to the location of the instruction file.
%{
#include <system/kzs_header.h>
#include <system/kzs_types.h>
#include "shared_data.hpp"
#include "android_swig_controller.hpp"
%}
%include "../../../../../../../Engine/headers/system/common/include/system/kzs_header.h"
%include "../../../../../../../Engine/headers/system/common/include/system/kzs_types.h"
%include "../../../../src/shared_data.hpp"
%include "../../../../src/android_swig_controller.hpp"@ECHO OFF SETLOCAL SET SWIG_PATH=..\..\..\..\..\..\..\Engine\tools\thirdparty\swig SET PROJECT_PATH=..\..\..\.. SET SWIG_INSTRUCTION_FILE=%PROJECT_PATH%\configs\swig\Android_SWIG.i SET WRAPPER_SOURCE_FILE=%PROJECT_PATH%\platforms\android\src\Android_SWIG_wrap.cpp SET JAVA_SOURCES_PATH=%PROJECT_PATH%\configs\platforms\android\src\com\rightware\kanzi\Android_SWIG SET PACKAGE=com.rightware.kanzi.Android_SWIG CALL %SWIG_PATH%\swig -java -verbose -c++ -outdir %JAVA_SOURCES_PATH% -package %PACKAGE% -o %WRAPPER_SOURCE_FILE% %SWIG_INSTRUCTION_FILE% ENDLOCAL
Now that the SharedData singleton is exposed to the Java side, you can start implementing the interfaces for the native logic and adding setters and getters for those.
In the example application, there is a class called AndroidSWIGController, which you can use to rotate the Android character and toggle lights in the scene.
Below is an example of this implementation in the android_swig_controller.hpp.
class
AndroidSWIGController
{
public:
AndroidSWIGController(struct KzuProject* project);
// Set the rotation property (0...1)
void setRotation(kzFloat value);
// Toggle lights in the scene
void toggleLights(kzBool isOn);
//...
For example, the setRotation function sets a custom property type value for a node:
void AndroidSWIGController::setRotation(kzFloat value)
{
struct KzuPropertyType* rotationPropertyType;
struct KzuObjectNode* androidNode;
kzuProjectLoaderLoadPropertyType(project, ROTATION_PROPERTY, &rotationPropertyType);
kzuProjectLoaderLoadObjectNode(project, ANDROID_NODE, &androidNode);
kzuObjectNodeSetFloatProperty(androidNode, rotationPropertyType, value);
}
Kanzi utilizes Android’s GLSurfaceView so it runs in GLThread. This means you have to call functions having Kanzi calls in GLThread. GLSurfaceView has a convenient function called queueEvent to do just that. Below is an example of this implementation in the AndroidSWIGActivity.java.
queueEvent(new Runnable() {
public void run() {
if (SharedData.get().getController() != null) SharedData.get().getController().setRotation((float)progress / 100);
}
});
To connect a call to Java from C++ you need to add a virtual function in the C++ interface, enable SWIG directors feature for that class, override the function in Java class, and provide the instance of that to the native side using the SharedData.
The example application uses this approach for two listeners to react to changes on the native side. The first one waits for the moment when the AndroidSWIGController is ready to be used. The second one listens for the rotation value changes to update a Java label. Both use the same approach.
Introduce a listener class:
class SharedDataListener
{
public:
// Constructs a SharedDataListener.
SharedDataListener() {};
// Destructs the SharedDataListener.
virtual ~SharedDataListener() {};
// This is a callback called when AndroidSWIGController is ready.
virtual void onControllerReady() {};
};
Add functions to register and unregister the listeners:
// Register a new listener void registerListener(SharedDataListener* listener); // Unregister a listener void unRegisterListener(SharedDataListener* listener);
Add director feature for the listener class in the SWIG instruction file:
%feature("director") SharedDataListener;
On the Java side, create and register the listener:
sharedDataListener = new SharedDataListener() {
// This function will be called from GL Thread
// This shows the Android UI when the AndroidSWIGController is ready.
// Note that as the onControllerReady is called from the GL Thread, the code affecting the Android UI cannot be called directly, but inside a runnable and using Activity’s runOnUiThread function.
public void onControllerReady()
{
// Register the observer to update the rotation value text view
androidSWIGListener = new AndroidSWIGListener() {
@Override
// This function will be called from GL Thread
public void onRotationChanged(final int newValue) {
// We need to run code affecting
Android UI in UI Thread AndroidSWIGActivity.this.runOnUiThread(new Runnable() {
public void run() {
TextView rotationValue = (TextView) controlsView.findViewById(R.id.rotation_value);
if (rotationValue != null)
rotationValue.setText(String.valueOf(newValue));
}
});
}
};
SharedData.get().getController().registerListener(androidSWIGListener);
// We need to run code affecting Android UI in UI Thread
AndroidSWIGActivity.this.runOnUiThread(new Runnable() {
public void run() {
AndroidSWIGActivity.this.addContentView(controlsView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
});
}
};
SharedData.get().registerListener(sharedDataListener);
Unregister the listener on onDestroy:
SharedData.get().unRegisterListener(sharedDataListener);