This is part 2 of my HowTo: SDL on Android series.
(prev) (next)
Working with the Android platform is not so different from desktop platforms. The biggest design difference is that the main input device is a touch screen. Android devices can also be rotated to change the screen orientation via a built-in accelerometer. There are also a few details that arise because we’re running native C or C++ code behind a Java front. So we’ll have to handle touch events and orientation changes in addition to logging, file I/O, and interfacing with Java.
Touch Input Events
SDL 2.0 has a new member in its event structure for touch events. We should at this point ignore mouse events (SDL_MOUSEBUTTONDOWN, etc.) and start responding to SDL_FINGERDOWN, SDL_FINGERUP, and SDL_FINGERMOTION event types. event.tfinger holds the data for all types of touch events.
It works mostly like mouse data, except that the positions (x, y, dx, dy) are normalized to [0.0, 1.0]. You have to multiply them by the window’s width or height to get window coordinates. This is so the concept of a touch device can be abstracted from the window. To fully support multitouch, you also have to pay attention to tfinger.touchId (the device ID, not as important) and tfinger.fingerId (an ID for this particular touch).
Orientation Events
In Part 1 of this guide, I briefly mentioned enabling orientation events by making sure to specify in AndroidManifest.xml that your app can handle that kind of configuration change. Once you have that, SDL pretty much just works how you’d hope. When your Android device is rotated, your app receives an SDL_WINDOWEVENT_SIZE_CHANGED window event. Note that window events in SDL2 are special events. They all have the same event type SDL_WINDOWEVENT, then you have to check the event.window.event member to see which kind of window event it is. The SDL_WINDOWEVENT_SIZE_CHANGED window event will have the new dimensions of the screen in the event.window.data1 and event.window.data2 members. You can check what kind of orientation this results in (landscape is wide vs. portrait is tall) by comparing the values of the width and height.
Often, though, you have an app for which it doesn’t make sense to switch between landscape and portrait orientations. You can lock the orientation with a specification in the AndroidManifest.xml. In the same ‘activity’ element that has the configChanges attribute, add another attribute:
android:screenOrientation="sensorLandscape"
or
android:screenOrientation="sensorPortait"
These will let the screen flip upside-down if the device is held upside-down, but still lock to the aspect that you choose. Notice that I use “sensorPortait” here, because for an older version of Android, someone made a typo.
Logging
Android uses a program called logcat to check the contents of the device log. You can use logcat directly through adb or you can use Eclipse’s logging window. Normal native apps typically use __android_log_print() or similar functions to print to the log. This support is pulled from a library, so you would need to include <android/log.h> and link with -llog. But… don’t bother. SDL has already done this for you. SDL_Log() is a direct replacement for ‘printf’ or ‘cout’. There are some other functions to help you filter your messages in logcat (check that wiki page). I also like to wrap SDL_LogMessageV() for more control.
Assets with SDL_RWops
If you haven’t used them before, SDL_RWops are awesome. They are an abstraction over reading and writing memory and can cover things like file access, in-memory loading, and even network data reading if you’re crazy. They bring low-level data into your program and if an interface supports SDL_RWops (e.g. SDL_LoadBMP_RW), you can turn that data stream into something immediately useful.
What this means for us in Android may not be obvious right away. The SDL_RWops creation functions (like SDL_RWFromFile) will first check *within* your APK, in its assets/ directory. Since thing like SDL_image’s IMG_Load() are built atop of SDL_RWops, this makes them work automatically even though your assets are technically inside a zip file. Just make sure to keep updated asset files in your project’s assets/ directory.
And if you’re using SDL_RWops directly, use SDL_RWread, SDL_RWwrite, and SDL_RWclose (which frees the SDL_RWops too). For reference, here’s an example of how to read in all the bytes from an SDL_RWops:
SDL_RWops* rwops = SDL_RWFromFile(filename, "rb");
if(rwops == NULL)
{
SDL_Log("Could not open file \"%s\".\n", filename);
return -1;
}
long data_max_size = 100;
unsigned char* data = (unsigned char*)malloc(data_max_size);
long total = 0;
long len = 0;
while((len = SDL_RWread(rwops, data, 1, data_max_size)) > 0)
{
// Do stuff with your 'len' bytes of data
total += len;
}
free(data);
SDL_RWclose(rwops);
User Data
If you need to read or write data on the Android device, you can’t (can, but shouldn’t) do it directly in the APK. It gets messy reading and writing inside a zip file (your APK). Instead, SDL gives you some native access to the user directory with SDL_AndroidGetInternalStoragePath(). This returns the path to the user data directory for your app. To specify a file there, append a ‘/’ and the file name. You can use an SDL_RWops to open the file and you’re set.
Interfacing with Java
Typically, getting two different languages to communicate is verbose, messy, and error-prone. Java and C are reasonably similar on the surface… but no, this will still be messy.
Java uses JNI (Java Native Interface) to communicate with C or C++. JNI works both directions. You can make native calls from Java (Java -> C) and you can make Java calls from the native side (C -> Java). I, for one, wish it weren’t necessary, but there are some APIs that do not have a native implementation.
The gist of Java -> C is that Java needs to know exactly how to call the desired C function. That means writing a declaration in Java and writing the called function in a special way in your native code. The nice thing is that we don’t have to modify SDLActivity.java at all to customize our app’s Java-side behavior. SDL gives us an “in” by way of inheriting from SDLActivity in our own Java source. We just have to make sure that we let SDL continue to do what it needs to do.
Here’s my example for my Test1 class if I wanted key down events from the OUYA controller API:
import tv.ouya.console.api.OuyaController;
public class Test1 extends SDLActivity
{
public static native void OuyaControllerKeyDown(int player, int keyCode); // Our native function declaration
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // Let SDL do its thing
OuyaController.init(this); // My turn
}
// I want to catch key events and send them to the native code
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
int keyCode = event.getKeyCode();
if(event.getAction() == KeyEvent.ACTION_UP && onKeyUp(keyCode, event)) // I'm not including onKeyUp() for brevity
return true;
else if(event.getAction() == KeyEvent.ACTION_DOWN && onKeyDown(keyCode, event))
return true;
return super.dispatchKeyEvent(event);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch(keyCode){
case OuyaController.BUTTON_O:
case OuyaController.BUTTON_U:
case OuyaController.BUTTON_Y:
case OuyaController.BUTTON_A:
case OuyaController.BUTTON_L1:
case OuyaController.BUTTON_R1:
case OuyaController.BUTTON_L3:
case OuyaController.BUTTON_R3:
case OuyaController.BUTTON_MENU:
case OuyaController.BUTTON_DPAD_UP:
case OuyaController.BUTTON_DPAD_RIGHT:
case OuyaController.BUTTON_DPAD_DOWN:
case OuyaController.BUTTON_DPAD_LEFT:
if(event.getRepeatCount() == 0)
OuyaControllerKeyDown(OuyaController.getPlayerNumByDeviceId(event.getDeviceId()), keyCode); // The native call
return true;
}
return super.onKeyDown(keyCode, event);
}
}
So, I declare my native function as public static native (and don’t use underscores! You’ll see why soon.) and I make sure to pass control to the superclass by calling its corresponding super.* call whenever I override a method.
Now, the native side:
#ifdef __ANDROID__
#include <jni.h>
extern "C" void Java_com_dinomage_test1_Test1_OuyaControllerKeyDown(JNIEnv* env, jclass cls, jint player, jint keyCode)
{
__android_log_print(ANDROID_LOG_VERBOSE, "Test1", "nativeOuyaControllerKeyDown(): player=%d, keyCode=%d", player, keyCode);
// We got data!
}
#endif
C++ is a little naughty with its name-mangling, so declaring a function with C linkage (extern “C”) is pretty common. Java wants us to do that here so it can find the function to call in our binary (a shared object on Android – that’s like a DLL in Windows). In C, of course, you don’t need to do that. Java also wants us to name the function very specifically. You’ll notice that the underscores separate a uniquely identifying set of tokens. “com.dinomage.test1” is the package/module name. “Test1” is the Java class that has the native function declaration. “OuyaControllerKeyDown” is the function name, finally.
JNI uses some special types in the native code so we can access the data. Some are simple and obvious, like “jint”. We can use it just like an integer, because it is one. Some aren’t so simple. With “jstring”, we would need to call a special function to extract the C string and then another to free it later. Java does not store data the same way that either C or C++ do. See the JNI reference for details.
That’s that for Java -> C.
C -> Java: TODO!
If you have anything to add, please let me know in the comments!
Tags: android, SDL, SDL2