src/core/android/SDL_android.cpp
author Tim Angus <tim@blackcompanystudios.co.uk>
Fri, 14 Oct 2011 17:29:49 +0100
changeset 5993 c9cb52d6d864
parent 5982 f324bd81b52c
child 5994 153d15ab3032
permissions -rw-r--r--
* Android's InputStream::skip is apparently buggy, so instead read into a dummy buffer
     1 /*
     2   Simple DirectMedia Layer
     3   Copyright (C) 1997-2011 Sam Lantinga <slouken@libsdl.org>
     4 
     5   This software is provided 'as-is', without any express or implied
     6   warranty.  In no event will the authors be held liable for any damages
     7   arising from the use of this software.
     8 
     9   Permission is granted to anyone to use this software for any purpose,
    10   including commercial applications, and to alter it and redistribute it
    11   freely, subject to the following restrictions:
    12 
    13   1. The origin of this software must not be misrepresented; you must not
    14      claim that you wrote the original software. If you use this software
    15      in a product, an acknowledgment in the product documentation would be
    16      appreciated but is not required.
    17   2. Altered source versions must be plainly marked as such, and must not be
    18      misrepresented as being the original software.
    19   3. This notice may not be removed or altered from any source distribution.
    20 */
    21 #include "SDL_config.h"
    22 #include "SDL_stdinc.h"
    23 
    24 #include "SDL_android.h"
    25 
    26 #include <algorithm>
    27 
    28 extern "C" {
    29 #include "../../events/SDL_events_c.h"
    30 #include "../../video/android/SDL_androidkeyboard.h"
    31 #include "../../video/android/SDL_androidtouch.h"
    32 #include "../../video/android/SDL_androidvideo.h"
    33 
    34 /* Impelemented in audio/android/SDL_androidaudio.c */
    35 extern void Android_RunAudioThread();
    36 } // C
    37 
    38 /*******************************************************************************
    39  This file links the Java side of Android with libsdl
    40 *******************************************************************************/
    41 #include <jni.h>
    42 #include <android/log.h>
    43 
    44 
    45 /*******************************************************************************
    46                                Globals
    47 *******************************************************************************/
    48 static JNIEnv* mEnv = NULL;
    49 static JNIEnv* mAudioEnv = NULL;
    50 
    51 // Main activity
    52 static jclass mActivityClass;
    53 
    54 // method signatures
    55 static jmethodID midCreateGLContext;
    56 static jmethodID midFlipBuffers;
    57 static jmethodID midAudioInit;
    58 static jmethodID midAudioWriteShortBuffer;
    59 static jmethodID midAudioWriteByteBuffer;
    60 static jmethodID midAudioQuit;
    61 
    62 // Accelerometer data storage
    63 static float fLastAccelerometer[3];
    64 
    65 
    66 /*******************************************************************************
    67                  Functions called by JNI
    68 *******************************************************************************/
    69 
    70 // Library init
    71 extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
    72 {
    73     return JNI_VERSION_1_4;
    74 }
    75 
    76 // Called before SDL_main() to initialize JNI bindings
    77 extern "C" void SDL_Android_Init(JNIEnv* env, jclass cls)
    78 {
    79     __android_log_print(ANDROID_LOG_INFO, "SDL", "SDL_Android_Init()");
    80 
    81     mEnv = env;
    82     mActivityClass = cls;
    83 
    84     midCreateGLContext = mEnv->GetStaticMethodID(mActivityClass,
    85                                 "createGLContext","(II)Z");
    86     midFlipBuffers = mEnv->GetStaticMethodID(mActivityClass,
    87                                 "flipBuffers","()V");
    88     midAudioInit = mEnv->GetStaticMethodID(mActivityClass, 
    89                                 "audioInit", "(IZZI)Ljava/lang/Object;");
    90     midAudioWriteShortBuffer = mEnv->GetStaticMethodID(mActivityClass,
    91                                 "audioWriteShortBuffer", "([S)V");
    92     midAudioWriteByteBuffer = mEnv->GetStaticMethodID(mActivityClass,
    93                                 "audioWriteByteBuffer", "([B)V");
    94     midAudioQuit = mEnv->GetStaticMethodID(mActivityClass,
    95                                 "audioQuit", "()V");
    96 
    97     if(!midCreateGLContext || !midFlipBuffers || !midAudioInit ||
    98        !midAudioWriteShortBuffer || !midAudioWriteByteBuffer || !midAudioQuit) {
    99         __android_log_print(ANDROID_LOG_WARN, "SDL", "SDL: Couldn't locate Java callbacks, check that they're named and typed correctly");
   100     }
   101 }
   102 
   103 // Resize
   104 extern "C" void Java_org_libsdl_app_SDLActivity_onNativeResize(
   105                                     JNIEnv* env, jclass jcls,
   106                                     jint width, jint height, jint format)
   107 {
   108     Android_SetScreenResolution(width, height, format);
   109 }
   110 
   111 // Keydown
   112 extern "C" void Java_org_libsdl_app_SDLActivity_onNativeKeyDown(
   113                                     JNIEnv* env, jclass jcls, jint keycode)
   114 {
   115     Android_OnKeyDown(keycode);
   116 }
   117 
   118 // Keyup
   119 extern "C" void Java_org_libsdl_app_SDLActivity_onNativeKeyUp(
   120                                     JNIEnv* env, jclass jcls, jint keycode)
   121 {
   122     Android_OnKeyUp(keycode);
   123 }
   124 
   125 // Touch
   126 extern "C" void Java_org_libsdl_app_SDLActivity_onNativeTouch(
   127                                     JNIEnv* env, jclass jcls,
   128                                     jint touch_device_id_in, jint pointer_finger_id_in,
   129                                     jint action, jfloat x, jfloat y, jfloat p)
   130 {
   131     Android_OnTouch(touch_device_id_in, pointer_finger_id_in, action, x, y, p);
   132 }
   133 
   134 // Accelerometer
   135 extern "C" void Java_org_libsdl_app_SDLActivity_onNativeAccel(
   136                                     JNIEnv* env, jclass jcls,
   137                                     jfloat x, jfloat y, jfloat z)
   138 {
   139     fLastAccelerometer[0] = x;
   140     fLastAccelerometer[1] = y;
   141     fLastAccelerometer[2] = z;   
   142 }
   143 
   144 // Quit
   145 extern "C" void Java_org_libsdl_app_SDLActivity_nativeQuit(
   146                                     JNIEnv* env, jclass cls)
   147 {    
   148     // Inject a SDL_QUIT event
   149     SDL_SendQuit();
   150 }
   151 
   152 extern "C" void Java_org_libsdl_app_SDLActivity_nativeRunAudioThread(
   153                                     JNIEnv* env, jclass cls)
   154 {
   155     /* This is the audio thread, with a different environment */
   156     mAudioEnv = env;
   157 
   158     Android_RunAudioThread();
   159 }
   160 
   161 
   162 /*******************************************************************************
   163              Functions called by SDL into Java
   164 *******************************************************************************/
   165 extern "C" SDL_bool Android_JNI_CreateContext(int majorVersion, int minorVersion)
   166 {
   167     if (mEnv->CallStaticBooleanMethod(mActivityClass, midCreateGLContext, majorVersion, minorVersion)) {
   168         return SDL_TRUE;
   169     } else {
   170         return SDL_FALSE;
   171     }
   172 }
   173 
   174 extern "C" void Android_JNI_SwapWindow()
   175 {
   176     mEnv->CallStaticVoidMethod(mActivityClass, midFlipBuffers); 
   177 }
   178 
   179 extern "C" void Android_JNI_SetActivityTitle(const char *title)
   180 {
   181     jmethodID mid;
   182 
   183     mid = mEnv->GetStaticMethodID(mActivityClass,"setActivityTitle","(Ljava/lang/String;)V");
   184     if (mid) {
   185         mEnv->CallStaticVoidMethod(mActivityClass, mid, mEnv->NewStringUTF(title));
   186     }
   187 }
   188 
   189 extern "C" void Android_JNI_GetAccelerometerValues(float values[3])
   190 {
   191     int i;
   192     for (i = 0; i < 3; ++i) {
   193         values[i] = fLastAccelerometer[i];
   194     }
   195 }
   196 
   197 //
   198 // Audio support
   199 //
   200 static jboolean audioBuffer16Bit = JNI_FALSE;
   201 static jboolean audioBufferStereo = JNI_FALSE;
   202 static jobject audioBuffer = NULL;
   203 static void* audioBufferPinned = NULL;
   204 
   205 extern "C" int Android_JNI_OpenAudioDevice(int sampleRate, int is16Bit, int channelCount, int desiredBufferFrames)
   206 {
   207     int audioBufferFrames;
   208 
   209     __android_log_print(ANDROID_LOG_VERBOSE, "SDL", "SDL audio: opening device");
   210     audioBuffer16Bit = is16Bit;
   211     audioBufferStereo = channelCount > 1;
   212 
   213     audioBuffer = mEnv->CallStaticObjectMethod(mActivityClass, midAudioInit, sampleRate, audioBuffer16Bit, audioBufferStereo, desiredBufferFrames);
   214 
   215     if (audioBuffer == NULL) {
   216         __android_log_print(ANDROID_LOG_WARN, "SDL", "SDL audio: didn't get back a good audio buffer!");
   217         return 0;
   218     }
   219     audioBuffer = mEnv->NewGlobalRef(audioBuffer);
   220 
   221     jboolean isCopy = JNI_FALSE;
   222     if (audioBuffer16Bit) {
   223         audioBufferPinned = mEnv->GetShortArrayElements((jshortArray)audioBuffer, &isCopy);
   224         audioBufferFrames = mEnv->GetArrayLength((jshortArray)audioBuffer);
   225     } else {
   226         audioBufferPinned = mEnv->GetByteArrayElements((jbyteArray)audioBuffer, &isCopy);
   227         audioBufferFrames = mEnv->GetArrayLength((jbyteArray)audioBuffer);
   228     }
   229     if (audioBufferStereo) {
   230         audioBufferFrames /= 2;
   231     }
   232 
   233     return audioBufferFrames;
   234 }
   235 
   236 extern "C" void * Android_JNI_GetAudioBuffer()
   237 {
   238     return audioBufferPinned;
   239 }
   240 
   241 extern "C" void Android_JNI_WriteAudioBuffer()
   242 {
   243     if (audioBuffer16Bit) {
   244         mAudioEnv->ReleaseShortArrayElements((jshortArray)audioBuffer, (jshort *)audioBufferPinned, JNI_COMMIT);
   245         mAudioEnv->CallStaticVoidMethod(mActivityClass, midAudioWriteShortBuffer, (jshortArray)audioBuffer);
   246     } else {
   247         mAudioEnv->ReleaseByteArrayElements((jbyteArray)audioBuffer, (jbyte *)audioBufferPinned, JNI_COMMIT);
   248         mAudioEnv->CallStaticVoidMethod(mActivityClass, midAudioWriteByteBuffer, (jbyteArray)audioBuffer);
   249     }
   250 
   251     /* JNI_COMMIT means the changes are committed to the VM but the buffer remains pinned */
   252 }
   253 
   254 extern "C" void Android_JNI_CloseAudioDevice()
   255 {
   256     mEnv->CallStaticVoidMethod(mActivityClass, midAudioQuit); 
   257 
   258     if (audioBuffer) {
   259         mEnv->DeleteGlobalRef(audioBuffer);
   260         audioBuffer = NULL;
   261         audioBufferPinned = NULL;
   262     }
   263 }
   264 
   265 // Test for an exception and call SDL_SetError with its detail if one occurs
   266 static bool Android_JNI_ExceptionOccurred()
   267 {
   268     jthrowable exception = mEnv->ExceptionOccurred();
   269     if (exception != NULL) {
   270         jmethodID mid;
   271 
   272         // Until this happens most JNI operations have undefined behaviour
   273         mEnv->ExceptionClear();
   274 
   275         jclass exceptionClass = mEnv->GetObjectClass(exception);
   276         jclass classClass = mEnv->FindClass("java/lang/Class");
   277 
   278         mid = mEnv->GetMethodID(classClass, "getName", "()Ljava/lang/String;");
   279         jstring exceptionName = (jstring)mEnv->CallObjectMethod(exceptionClass, mid);
   280         const char* exceptionNameUTF8 = mEnv->GetStringUTFChars(exceptionName, 0);
   281 
   282         mid = mEnv->GetMethodID(exceptionClass, "getMessage", "()Ljava/lang/String;");
   283         jstring exceptionMessage = (jstring)mEnv->CallObjectMethod(exception, mid);
   284 
   285         if (exceptionMessage != NULL) {
   286             const char* exceptionMessageUTF8 = mEnv->GetStringUTFChars(
   287                     exceptionMessage, 0);
   288             SDL_SetError("%s: %s", exceptionNameUTF8, exceptionMessageUTF8);
   289             mEnv->ReleaseStringUTFChars(exceptionMessage, exceptionMessageUTF8);
   290             mEnv->DeleteLocalRef(exceptionMessage);
   291         } else {
   292             SDL_SetError("%s", exceptionNameUTF8);
   293         }
   294 
   295         mEnv->ReleaseStringUTFChars(exceptionName, exceptionNameUTF8);
   296         mEnv->DeleteLocalRef(exceptionName);
   297         mEnv->DeleteLocalRef(classClass);
   298         mEnv->DeleteLocalRef(exceptionClass);
   299         mEnv->DeleteLocalRef(exception);
   300 
   301         return true;
   302     }
   303 
   304     return false;
   305 }
   306 
   307 static int Android_JNI_FileOpen(SDL_RWops* ctx)
   308 {
   309     int result = 0;
   310 
   311     jmethodID mid;
   312     jobject context;
   313     jobject assetManager;
   314     jobject inputStream;
   315     jclass channels;
   316     jobject readableByteChannel;
   317     jstring fileNameJString;
   318 
   319     bool allocatedLocalFrame = false;
   320 
   321     if (mEnv->PushLocalFrame(16) < 0) {
   322         SDL_SetError("Failed to allocate enough JVM local references");
   323         goto failure;
   324     } else {
   325         allocatedLocalFrame = true;
   326     }
   327 
   328     fileNameJString = (jstring)ctx->hidden.androidio.fileName;
   329 
   330     // context = SDLActivity.getContext();
   331     mid = mEnv->GetStaticMethodID(mActivityClass,
   332             "getContext","()Landroid/content/Context;");
   333     context = mEnv->CallStaticObjectMethod(mActivityClass, mid);
   334 
   335     // assetManager = context.getAssets();
   336     mid = mEnv->GetMethodID(mEnv->GetObjectClass(context),
   337             "getAssets", "()Landroid/content/res/AssetManager;");
   338     assetManager = mEnv->CallObjectMethod(context, mid);
   339 
   340     // inputStream = assetManager.open(<filename>);
   341     mid = mEnv->GetMethodID(mEnv->GetObjectClass(assetManager),
   342             "open", "(Ljava/lang/String;)Ljava/io/InputStream;");
   343     inputStream = mEnv->CallObjectMethod(assetManager, mid, fileNameJString);
   344     if (Android_JNI_ExceptionOccurred()) {
   345         goto failure;
   346     }
   347 
   348     ctx->hidden.androidio.inputStream = inputStream;
   349     ctx->hidden.androidio.inputStreamRef = mEnv->NewGlobalRef(inputStream);
   350 
   351     // Despite all the visible documentation on [Asset]InputStream claiming
   352     // that the .available() method is not guaranteed to return the entire file
   353     // size, comments in <sdk>/samples/<ver>/ApiDemos/src/com/example/ ...
   354     // android/apis/content/ReadAsset.java imply that Android's
   355     // AssetInputStream.available() /will/ always return the total file size
   356 
   357     // size = inputStream.available();
   358     mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
   359             "available", "()I");
   360     ctx->hidden.androidio.size = mEnv->CallIntMethod(inputStream, mid);
   361     if (Android_JNI_ExceptionOccurred()) {
   362         goto failure;
   363     }
   364 
   365     // readableByteChannel = Channels.newChannel(inputStream);
   366     channels = mEnv->FindClass("java/nio/channels/Channels");
   367     mid = mEnv->GetStaticMethodID(channels,
   368             "newChannel",
   369             "(Ljava/io/InputStream;)Ljava/nio/channels/ReadableByteChannel;");
   370     readableByteChannel = mEnv->CallStaticObjectMethod(
   371             channels, mid, inputStream);
   372     if (Android_JNI_ExceptionOccurred()) {
   373         goto failure;
   374     }
   375 
   376     ctx->hidden.androidio.readableByteChannel = readableByteChannel;
   377     ctx->hidden.androidio.readableByteChannelRef =
   378         mEnv->NewGlobalRef(readableByteChannel);
   379 
   380     // Store .read id for reading purposes
   381     mid = mEnv->GetMethodID(mEnv->GetObjectClass(readableByteChannel),
   382             "read", "(Ljava/nio/ByteBuffer;)I");
   383     ctx->hidden.androidio.readMethod = mid;
   384 
   385     ctx->hidden.androidio.position = 0;
   386 
   387     if (false) {
   388 failure:
   389         result = -1;
   390 
   391         mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.fileNameRef);
   392 
   393         if(ctx->hidden.androidio.inputStreamRef != NULL) {
   394             mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.inputStreamRef);
   395         }
   396     }
   397 
   398     if (allocatedLocalFrame) {
   399         mEnv->PopLocalFrame(NULL);
   400     }
   401 
   402     return result;
   403 }
   404 
   405 extern "C" int Android_JNI_FileOpen(SDL_RWops* ctx,
   406         const char* fileName, const char*)
   407 {
   408     if (!ctx) {
   409         return -1;
   410     }
   411 
   412     jstring fileNameJString = mEnv->NewStringUTF(fileName);
   413     ctx->hidden.androidio.fileName = fileNameJString;
   414     ctx->hidden.androidio.fileNameRef = mEnv->NewGlobalRef(fileNameJString);
   415     ctx->hidden.androidio.inputStreamRef = NULL;
   416     mEnv->DeleteLocalRef(fileNameJString);
   417 
   418     return Android_JNI_FileOpen(ctx);
   419 }
   420 
   421 extern "C" size_t Android_JNI_FileRead(SDL_RWops* ctx, void* buffer,
   422         size_t size, size_t maxnum)
   423 {
   424     int bytesRemaining = size * maxnum;
   425     int bytesRead = 0;
   426 
   427     jobject readableByteChannel = (jobject)ctx->hidden.androidio.readableByteChannel;
   428     jmethodID readMethod = (jmethodID)ctx->hidden.androidio.readMethod;
   429     jobject byteBuffer = mEnv->NewDirectByteBuffer(buffer, bytesRemaining);
   430 
   431     while (bytesRemaining > 0) {
   432         // result = readableByteChannel.read(...);
   433         int result = mEnv->CallIntMethod(readableByteChannel, readMethod, byteBuffer);
   434 
   435         if (Android_JNI_ExceptionOccurred()) {
   436             mEnv->DeleteLocalRef(byteBuffer);
   437             return 0;
   438         }
   439 
   440         if (result < 0) {
   441             break;
   442         }
   443 
   444         bytesRemaining -= result;
   445         bytesRead += result;
   446         ctx->hidden.androidio.position += result;
   447     }
   448 
   449     mEnv->DeleteLocalRef(byteBuffer);
   450 
   451     return bytesRead / size;
   452 }
   453 
   454 extern "C" size_t Android_JNI_FileWrite(SDL_RWops* ctx, const void* buffer,
   455         size_t size, size_t num)
   456 {
   457     SDL_SetError("Cannot write to Android package filesystem");
   458     return 0;
   459 }
   460 
   461 static int Android_JNI_FileClose(SDL_RWops* ctx, bool release)
   462 {
   463     int result = 0;
   464 
   465     if (ctx) {
   466         if (release) {
   467             mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.fileNameRef);
   468         }
   469 
   470         jobject inputStream = (jobject)ctx->hidden.androidio.inputStream;
   471 
   472         // inputStream.close();
   473         jmethodID mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
   474                 "close", "()V");
   475         mEnv->CallVoidMethod(inputStream, mid);
   476         mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.inputStreamRef);
   477         mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.readableByteChannelRef);
   478         if (Android_JNI_ExceptionOccurred()) {
   479             result = -1;
   480         }
   481 
   482         if (release) {
   483             SDL_FreeRW(ctx);
   484         }
   485     }
   486 
   487     return result;
   488 }
   489 
   490 
   491 extern "C" long Android_JNI_FileSeek(SDL_RWops* ctx, long offset, int whence)
   492 {
   493     long newPosition;
   494 
   495     switch (whence) {
   496         case RW_SEEK_SET:
   497             newPosition = offset;
   498             break;
   499         case RW_SEEK_CUR:
   500             newPosition = ctx->hidden.androidio.position + offset;
   501             break;
   502         case RW_SEEK_END:
   503             newPosition = ctx->hidden.androidio.size + offset;
   504             break;
   505         default:
   506             SDL_SetError("Unknown value for 'whence'");
   507             return -1;
   508     }
   509     if (newPosition < 0) {
   510         newPosition = 0;
   511     }
   512     if (newPosition > ctx->hidden.androidio.size) {
   513         newPosition = ctx->hidden.androidio.size;
   514     }
   515 
   516     long movement = newPosition - ctx->hidden.androidio.position;
   517     jobject inputStream = (jobject)ctx->hidden.androidio.inputStream;
   518 
   519     if (movement > 0) {
   520         unsigned char buffer[1024];
   521 
   522         // The easy case where we're seeking forwards
   523         while (movement > 0) {
   524             size_t result = Android_JNI_FileRead(ctx, buffer, 1,
   525                 std::min(movement, (long)sizeof(buffer)));
   526 
   527             if (result <= 0) {
   528                 // Failed to read/skip the required amount, so fail
   529                 return -1;
   530             }
   531 
   532             movement -= result;
   533         }
   534     } else if (movement < 0) {
   535         // We can't seek backwards so we have to reopen the file and seek
   536         // forwards which obviously isn't very efficient
   537         Android_JNI_FileClose(ctx, false);
   538         Android_JNI_FileOpen(ctx);
   539         Android_JNI_FileSeek(ctx, newPosition, RW_SEEK_SET);
   540     }
   541 
   542     ctx->hidden.androidio.position = newPosition;
   543 
   544     return ctx->hidden.androidio.position;
   545 }
   546 
   547 extern "C" int Android_JNI_FileClose(SDL_RWops* ctx)
   548 {
   549     return Android_JNI_FileClose(ctx, true);
   550 }
   551 
   552 /* vi: set ts=4 sw=4 expandtab: */