src/core/android/SDL_android.cpp
author Sam Lantinga <slouken@libsdl.org>
Wed, 03 Oct 2012 20:49:16 -0700
changeset 6555 f2c03c06d987
parent 6464 ab55284b389f
child 6630 55910871076b
permissions -rw-r--r--
Fixed bug 1614 - SDL for Android does not implement TextInput API

Andrey Isakov 2012-10-03 08:30:25 PDT

I've found out in the process of porting one OS project to Android/SDL2 that
there is no support for TextInput events/APIs on Android.
So I implemented some kind of initial support of that feature, and at the very
least it seems to work fine with latin chars input with soft and hardware
keyboards on my Moto Milestone2. I've also tried playing around with more
complex IMEs, like japanese, logging the process and it seemed to work too. I'm
not sure since the app itself I am working on does not have support for
non-latin input.

The main point of the patch is to place a fake input view in the region
specified by SDL_SetTextInputRect and create a custom InputConnection for it.
The reason to make it a separate view is to support Android's pan&scan on input
feature properly. For details please refer to
http://android-developers.blogspot.com/2009/04/updating-applications-for-on-screen.html
Even though the manual states that SetTextInputRect is used to determine the
IME variants position, I thought this would be a proper use for this too.
     1 /*
     2   Simple DirectMedia Layer
     3   Copyright (C) 1997-2012 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 #include "SDL_assert.h"
    24 
    25 #ifdef __ANDROID__
    26 
    27 #include "SDL_android.h"
    28 
    29 extern "C" {
    30 #include "../../events/SDL_events_c.h"
    31 #include "../../video/android/SDL_androidkeyboard.h"
    32 #include "../../video/android/SDL_androidtouch.h"
    33 #include "../../video/android/SDL_androidvideo.h"
    34 
    35 #include <android/log.h>
    36 #include <pthread.h>
    37 #define LOG_TAG "SDL_android"
    38 //#define LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
    39 //#define LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
    40 #define LOGI(...) do {} while (false)
    41 #define LOGE(...) do {} while (false)
    42 
    43 
    44 /* Implemented in audio/android/SDL_androidaudio.c */
    45 extern void Android_RunAudioThread();
    46 } // C
    47 
    48 /*******************************************************************************
    49  This file links the Java side of Android with libsdl
    50 *******************************************************************************/
    51 #include <jni.h>
    52 #include <android/log.h>
    53 
    54 
    55 /*******************************************************************************
    56                                Globals
    57 *******************************************************************************/
    58 static pthread_key_t mThreadKey;
    59 static JavaVM* mJavaVM;
    60 
    61 // Main activity
    62 static jclass mActivityClass;
    63 
    64 // method signatures
    65 static jmethodID midCreateGLContext;
    66 static jmethodID midFlipBuffers;
    67 static jmethodID midAudioInit;
    68 static jmethodID midAudioWriteShortBuffer;
    69 static jmethodID midAudioWriteByteBuffer;
    70 static jmethodID midAudioQuit;
    71 
    72 // Accelerometer data storage
    73 static float fLastAccelerometer[3];
    74 static bool bHasNewData;
    75 
    76 /*******************************************************************************
    77                  Functions called by JNI
    78 *******************************************************************************/
    79 
    80 // Library init
    81 extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
    82 {
    83     JNIEnv *env;
    84     mJavaVM = vm;
    85     LOGI("JNI_OnLoad called");
    86     if (mJavaVM->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
    87         LOGE("Failed to get the environment using GetEnv()");
    88         return -1;
    89     }
    90     /*
    91      * Create mThreadKey so we can keep track of the JNIEnv assigned to each thread
    92      * Refer to http://developer.android.com/guide/practices/design/jni.html for the rationale behind this
    93      */
    94     if (pthread_key_create(&mThreadKey, Android_JNI_ThreadDestroyed)) {
    95         __android_log_print(ANDROID_LOG_ERROR, "SDL", "Error initializing pthread key");
    96     }
    97     else {
    98         Android_JNI_SetupThread();
    99     }
   100 
   101     return JNI_VERSION_1_4;
   102 }
   103 
   104 // Called before SDL_main() to initialize JNI bindings
   105 extern "C" void SDL_Android_Init(JNIEnv* mEnv, jclass cls)
   106 {
   107     __android_log_print(ANDROID_LOG_INFO, "SDL", "SDL_Android_Init()");
   108 
   109     Android_JNI_SetupThread();
   110 
   111     mActivityClass = (jclass)mEnv->NewGlobalRef(cls);
   112 
   113     midCreateGLContext = mEnv->GetStaticMethodID(mActivityClass,
   114                                 "createGLContext","(II)Z");
   115     midFlipBuffers = mEnv->GetStaticMethodID(mActivityClass,
   116                                 "flipBuffers","()V");
   117     midAudioInit = mEnv->GetStaticMethodID(mActivityClass, 
   118                                 "audioInit", "(IZZI)Ljava/lang/Object;");
   119     midAudioWriteShortBuffer = mEnv->GetStaticMethodID(mActivityClass,
   120                                 "audioWriteShortBuffer", "([S)V");
   121     midAudioWriteByteBuffer = mEnv->GetStaticMethodID(mActivityClass,
   122                                 "audioWriteByteBuffer", "([B)V");
   123     midAudioQuit = mEnv->GetStaticMethodID(mActivityClass,
   124                                 "audioQuit", "()V");
   125 
   126     bHasNewData = false;
   127 
   128     if(!midCreateGLContext || !midFlipBuffers || !midAudioInit ||
   129        !midAudioWriteShortBuffer || !midAudioWriteByteBuffer || !midAudioQuit) {
   130         __android_log_print(ANDROID_LOG_WARN, "SDL", "SDL: Couldn't locate Java callbacks, check that they're named and typed correctly");
   131     }
   132     __android_log_print(ANDROID_LOG_INFO, "SDL", "SDL_Android_Init() finished!");
   133 }
   134 
   135 // Resize
   136 extern "C" void Java_org_libsdl_app_SDLActivity_onNativeResize(
   137                                     JNIEnv* env, jclass jcls,
   138                                     jint width, jint height, jint format)
   139 {
   140     Android_SetScreenResolution(width, height, format);
   141 }
   142 
   143 // Keydown
   144 extern "C" void Java_org_libsdl_app_SDLActivity_onNativeKeyDown(
   145                                     JNIEnv* env, jclass jcls, jint keycode)
   146 {
   147     Android_OnKeyDown(keycode);
   148 }
   149 
   150 // Keyup
   151 extern "C" void Java_org_libsdl_app_SDLActivity_onNativeKeyUp(
   152                                     JNIEnv* env, jclass jcls, jint keycode)
   153 {
   154     Android_OnKeyUp(keycode);
   155 }
   156 
   157 // Touch
   158 extern "C" void Java_org_libsdl_app_SDLActivity_onNativeTouch(
   159                                     JNIEnv* env, jclass jcls,
   160                                     jint touch_device_id_in, jint pointer_finger_id_in,
   161                                     jint action, jfloat x, jfloat y, jfloat p)
   162 {
   163     Android_OnTouch(touch_device_id_in, pointer_finger_id_in, action, x, y, p);
   164 }
   165 
   166 // Accelerometer
   167 extern "C" void Java_org_libsdl_app_SDLActivity_onNativeAccel(
   168                                     JNIEnv* env, jclass jcls,
   169                                     jfloat x, jfloat y, jfloat z)
   170 {
   171     fLastAccelerometer[0] = x;
   172     fLastAccelerometer[1] = y;
   173     fLastAccelerometer[2] = z;
   174     bHasNewData = true;
   175 }
   176 
   177 // Quit
   178 extern "C" void Java_org_libsdl_app_SDLActivity_nativeQuit(
   179                                     JNIEnv* env, jclass cls)
   180 {    
   181     // Inject a SDL_QUIT event
   182     SDL_SendQuit();
   183 }
   184 
   185 // Pause
   186 extern "C" void Java_org_libsdl_app_SDLActivity_nativePause(
   187                                     JNIEnv* env, jclass cls)
   188 {
   189     if (Android_Window) {
   190         /* Signal the pause semaphore so the event loop knows to pause and (optionally) block itself */
   191         if (!SDL_SemValue(Android_PauseSem)) SDL_SemPost(Android_PauseSem);
   192         SDL_SendWindowEvent(Android_Window, SDL_WINDOWEVENT_FOCUS_LOST, 0, 0);
   193         SDL_SendWindowEvent(Android_Window, SDL_WINDOWEVENT_MINIMIZED, 0, 0);
   194     }
   195 }
   196 
   197 // Resume
   198 extern "C" void Java_org_libsdl_app_SDLActivity_nativeResume(
   199                                     JNIEnv* env, jclass cls)
   200 {
   201     if (Android_Window) {
   202         /* Signal the resume semaphore so the event loop knows to resume and restore the GL Context
   203          * We can't restore the GL Context here because it needs to be done on the SDL main thread
   204          * and this function will be called from the Java thread instead.
   205          */
   206         if (!SDL_SemValue(Android_ResumeSem)) SDL_SemPost(Android_ResumeSem);
   207         SDL_SendWindowEvent(Android_Window, SDL_WINDOWEVENT_FOCUS_GAINED, 0, 0);
   208         SDL_SendWindowEvent(Android_Window, SDL_WINDOWEVENT_RESTORED, 0, 0);
   209     }
   210 }
   211 
   212 extern "C" void Java_org_libsdl_app_SDLActivity_nativeRunAudioThread(
   213                                     JNIEnv* env, jclass cls)
   214 {
   215     /* This is the audio thread, with a different environment */
   216     Android_JNI_SetupThread();
   217 
   218     Android_RunAudioThread();
   219 }
   220 
   221 extern "C" void Java_org_libsdl_app_SDLInputConnection_nativeCommitText(
   222                                     JNIEnv* env, jclass cls,
   223                                     jstring text, jint newCursorPosition)
   224 {
   225     const char *utftext = env->GetStringUTFChars(text, NULL);
   226 
   227     SDL_SendKeyboardText(utftext);
   228 
   229     env->ReleaseStringUTFChars(text, utftext);
   230 }
   231 
   232 extern "C" void Java_org_libsdl_app_SDLInputConnection_nativeSetComposingText(
   233                                     JNIEnv* env, jclass cls,
   234                                     jstring text, jint newCursorPosition)
   235 {
   236     const char *utftext = env->GetStringUTFChars(text, NULL);
   237 
   238     SDL_SendEditingText(utftext, 0, 0);
   239 
   240     env->ReleaseStringUTFChars(text, utftext);
   241 }
   242 
   243 
   244 
   245 
   246 /*******************************************************************************
   247              Functions called by SDL into Java
   248 *******************************************************************************/
   249 
   250 class LocalReferenceHolder
   251 {
   252 private:
   253     static int s_active;
   254 
   255 public:
   256     static bool IsActive() {
   257         return s_active > 0;
   258     }
   259 
   260 public:
   261     LocalReferenceHolder() : m_env(NULL) { }
   262     ~LocalReferenceHolder() {
   263         if (m_env) {
   264             m_env->PopLocalFrame(NULL);
   265             --s_active;
   266         }
   267     }
   268 
   269     bool init(JNIEnv *env, jint capacity = 16) {
   270         if (env->PushLocalFrame(capacity) < 0) {
   271             SDL_SetError("Failed to allocate enough JVM local references");
   272             return false;
   273         }
   274         ++s_active;
   275         m_env = env;
   276         return true;
   277     }
   278 
   279 protected:
   280     JNIEnv *m_env;
   281 };
   282 int LocalReferenceHolder::s_active;
   283 
   284 extern "C" SDL_bool Android_JNI_CreateContext(int majorVersion, int minorVersion)
   285 {
   286     JNIEnv *mEnv = Android_JNI_GetEnv();
   287     if (mEnv->CallStaticBooleanMethod(mActivityClass, midCreateGLContext, majorVersion, minorVersion)) {
   288         return SDL_TRUE;
   289     } else {
   290         return SDL_FALSE;
   291     }
   292 }
   293 
   294 extern "C" void Android_JNI_SwapWindow()
   295 {
   296     JNIEnv *mEnv = Android_JNI_GetEnv();
   297     mEnv->CallStaticVoidMethod(mActivityClass, midFlipBuffers); 
   298 }
   299 
   300 extern "C" void Android_JNI_SetActivityTitle(const char *title)
   301 {
   302     jmethodID mid;
   303     JNIEnv *mEnv = Android_JNI_GetEnv();
   304     mid = mEnv->GetStaticMethodID(mActivityClass,"setActivityTitle","(Ljava/lang/String;)V");
   305     if (mid) {
   306         jstring jtitle = reinterpret_cast<jstring>(mEnv->NewStringUTF(title));
   307         mEnv->CallStaticVoidMethod(mActivityClass, mid, jtitle);
   308         mEnv->DeleteLocalRef(jtitle);
   309     }
   310 }
   311 
   312 extern "C" SDL_bool Android_JNI_GetAccelerometerValues(float values[3])
   313 {
   314     int i;
   315     SDL_bool retval = SDL_FALSE;
   316 
   317     if (bHasNewData) {
   318         for (i = 0; i < 3; ++i) {
   319             values[i] = fLastAccelerometer[i];
   320         }
   321         bHasNewData = false;
   322         retval = SDL_TRUE;
   323     }
   324 
   325     return retval;
   326 }
   327 
   328 static void Android_JNI_ThreadDestroyed(void* value) {
   329     /* The thread is being destroyed, detach it from the Java VM and set the mThreadKey value to NULL as required */
   330     JNIEnv *env = (JNIEnv*) value;
   331     if (env != NULL) {
   332         mJavaVM->DetachCurrentThread();
   333         pthread_setspecific(mThreadKey, NULL);
   334     }
   335 }
   336 
   337 JNIEnv* Android_JNI_GetEnv(void) {
   338     /* From http://developer.android.com/guide/practices/jni.html
   339      * All threads are Linux threads, scheduled by the kernel.
   340      * They're usually started from managed code (using Thread.start), but they can also be created elsewhere and then
   341      * attached to the JavaVM. For example, a thread started with pthread_create can be attached with the
   342      * JNI AttachCurrentThread or AttachCurrentThreadAsDaemon functions. Until a thread is attached, it has no JNIEnv,
   343      * and cannot make JNI calls.
   344      * Attaching a natively-created thread causes a java.lang.Thread object to be constructed and added to the "main"
   345      * ThreadGroup, making it visible to the debugger. Calling AttachCurrentThread on an already-attached thread
   346      * is a no-op.
   347      * Note: You can call this function any number of times for the same thread, there's no harm in it
   348      */
   349 
   350     JNIEnv *env;
   351     int status = mJavaVM->AttachCurrentThread(&env, NULL);
   352     if(status < 0) {
   353         LOGE("failed to attach current thread");
   354         return 0;
   355     }
   356 
   357     return env;
   358 }
   359 
   360 int Android_JNI_SetupThread(void) {
   361     /* From http://developer.android.com/guide/practices/jni.html
   362      * Threads attached through JNI must call DetachCurrentThread before they exit. If coding this directly is awkward,
   363      * in Android 2.0 (Eclair) and higher you can use pthread_key_create to define a destructor function that will be
   364      * called before the thread exits, and call DetachCurrentThread from there. (Use that key with pthread_setspecific
   365      * to store the JNIEnv in thread-local-storage; that way it'll be passed into your destructor as the argument.)
   366      * Note: The destructor is not called unless the stored value is != NULL
   367      * Note: You can call this function any number of times for the same thread, there's no harm in it
   368      *       (except for some lost CPU cycles)
   369      */
   370     JNIEnv *env = Android_JNI_GetEnv();
   371     pthread_setspecific(mThreadKey, (void*) env);
   372     return 1;
   373 }
   374 
   375 //
   376 // Audio support
   377 //
   378 static jboolean audioBuffer16Bit = JNI_FALSE;
   379 static jboolean audioBufferStereo = JNI_FALSE;
   380 static jobject audioBuffer = NULL;
   381 static void* audioBufferPinned = NULL;
   382 
   383 extern "C" int Android_JNI_OpenAudioDevice(int sampleRate, int is16Bit, int channelCount, int desiredBufferFrames)
   384 {
   385     int audioBufferFrames;
   386 
   387     int status;
   388     JNIEnv *env = Android_JNI_GetEnv();
   389 
   390     if (!env) {
   391         LOGE("callback_handler: failed to attach current thread");
   392     }
   393     Android_JNI_SetupThread();
   394 
   395     
   396     __android_log_print(ANDROID_LOG_VERBOSE, "SDL", "SDL audio: opening device");
   397     audioBuffer16Bit = is16Bit;
   398     audioBufferStereo = channelCount > 1;
   399 
   400     audioBuffer = env->CallStaticObjectMethod(mActivityClass, midAudioInit, sampleRate, audioBuffer16Bit, audioBufferStereo, desiredBufferFrames);
   401 
   402     if (audioBuffer == NULL) {
   403         __android_log_print(ANDROID_LOG_WARN, "SDL", "SDL audio: didn't get back a good audio buffer!");
   404         return 0;
   405     }
   406     audioBuffer = env->NewGlobalRef(audioBuffer);
   407 
   408     jboolean isCopy = JNI_FALSE;
   409     if (audioBuffer16Bit) {
   410         audioBufferPinned = env->GetShortArrayElements((jshortArray)audioBuffer, &isCopy);
   411         audioBufferFrames = env->GetArrayLength((jshortArray)audioBuffer);
   412     } else {
   413         audioBufferPinned = env->GetByteArrayElements((jbyteArray)audioBuffer, &isCopy);
   414         audioBufferFrames = env->GetArrayLength((jbyteArray)audioBuffer);
   415     }
   416     if (audioBufferStereo) {
   417         audioBufferFrames /= 2;
   418     }
   419  
   420     return audioBufferFrames;
   421 }
   422 
   423 extern "C" void * Android_JNI_GetAudioBuffer()
   424 {
   425     return audioBufferPinned;
   426 }
   427 
   428 extern "C" void Android_JNI_WriteAudioBuffer()
   429 {
   430     JNIEnv *mAudioEnv = Android_JNI_GetEnv();
   431 
   432     if (audioBuffer16Bit) {
   433         mAudioEnv->ReleaseShortArrayElements((jshortArray)audioBuffer, (jshort *)audioBufferPinned, JNI_COMMIT);
   434         mAudioEnv->CallStaticVoidMethod(mActivityClass, midAudioWriteShortBuffer, (jshortArray)audioBuffer);
   435     } else {
   436         mAudioEnv->ReleaseByteArrayElements((jbyteArray)audioBuffer, (jbyte *)audioBufferPinned, JNI_COMMIT);
   437         mAudioEnv->CallStaticVoidMethod(mActivityClass, midAudioWriteByteBuffer, (jbyteArray)audioBuffer);
   438     }
   439 
   440     /* JNI_COMMIT means the changes are committed to the VM but the buffer remains pinned */
   441 }
   442 
   443 extern "C" void Android_JNI_CloseAudioDevice()
   444 {
   445     int status;
   446     JNIEnv *env = Android_JNI_GetEnv();
   447 
   448     env->CallStaticVoidMethod(mActivityClass, midAudioQuit); 
   449 
   450     if (audioBuffer) {
   451         env->DeleteGlobalRef(audioBuffer);
   452         audioBuffer = NULL;
   453         audioBufferPinned = NULL;
   454     }
   455 }
   456 
   457 // Test for an exception and call SDL_SetError with its detail if one occurs
   458 static bool Android_JNI_ExceptionOccurred()
   459 {
   460     SDL_assert(LocalReferenceHolder::IsActive());
   461     JNIEnv *mEnv = Android_JNI_GetEnv();
   462 
   463     jthrowable exception = mEnv->ExceptionOccurred();
   464     if (exception != NULL) {
   465         jmethodID mid;
   466 
   467         // Until this happens most JNI operations have undefined behaviour
   468         mEnv->ExceptionClear();
   469 
   470         jclass exceptionClass = mEnv->GetObjectClass(exception);
   471         jclass classClass = mEnv->FindClass("java/lang/Class");
   472 
   473         mid = mEnv->GetMethodID(classClass, "getName", "()Ljava/lang/String;");
   474         jstring exceptionName = (jstring)mEnv->CallObjectMethod(exceptionClass, mid);
   475         const char* exceptionNameUTF8 = mEnv->GetStringUTFChars(exceptionName, 0);
   476 
   477         mid = mEnv->GetMethodID(exceptionClass, "getMessage", "()Ljava/lang/String;");
   478         jstring exceptionMessage = (jstring)mEnv->CallObjectMethod(exception, mid);
   479 
   480         if (exceptionMessage != NULL) {
   481             const char* exceptionMessageUTF8 = mEnv->GetStringUTFChars(
   482                     exceptionMessage, 0);
   483             SDL_SetError("%s: %s", exceptionNameUTF8, exceptionMessageUTF8);
   484             mEnv->ReleaseStringUTFChars(exceptionMessage, exceptionMessageUTF8);
   485         } else {
   486             SDL_SetError("%s", exceptionNameUTF8);
   487         }
   488 
   489         mEnv->ReleaseStringUTFChars(exceptionName, exceptionNameUTF8);
   490 
   491         return true;
   492     }
   493 
   494     return false;
   495 }
   496 
   497 static int Android_JNI_FileOpen(SDL_RWops* ctx)
   498 {
   499     LocalReferenceHolder refs;
   500     int result = 0;
   501 
   502     jmethodID mid;
   503     jobject context;
   504     jobject assetManager;
   505     jobject inputStream;
   506     jclass channels;
   507     jobject readableByteChannel;
   508     jstring fileNameJString;
   509 
   510     JNIEnv *mEnv = Android_JNI_GetEnv();
   511     if (!refs.init(mEnv)) {
   512         goto failure;
   513     }
   514 
   515     fileNameJString = (jstring)ctx->hidden.androidio.fileNameRef;
   516 
   517     // context = SDLActivity.getContext();
   518     mid = mEnv->GetStaticMethodID(mActivityClass,
   519             "getContext","()Landroid/content/Context;");
   520     context = mEnv->CallStaticObjectMethod(mActivityClass, mid);
   521 
   522     // assetManager = context.getAssets();
   523     mid = mEnv->GetMethodID(mEnv->GetObjectClass(context),
   524             "getAssets", "()Landroid/content/res/AssetManager;");
   525     assetManager = mEnv->CallObjectMethod(context, mid);
   526 
   527     // inputStream = assetManager.open(<filename>);
   528     mid = mEnv->GetMethodID(mEnv->GetObjectClass(assetManager),
   529             "open", "(Ljava/lang/String;)Ljava/io/InputStream;");
   530     inputStream = mEnv->CallObjectMethod(assetManager, mid, fileNameJString);
   531     if (Android_JNI_ExceptionOccurred()) {
   532         goto failure;
   533     }
   534 
   535     ctx->hidden.androidio.inputStreamRef = mEnv->NewGlobalRef(inputStream);
   536 
   537     // Despite all the visible documentation on [Asset]InputStream claiming
   538     // that the .available() method is not guaranteed to return the entire file
   539     // size, comments in <sdk>/samples/<ver>/ApiDemos/src/com/example/ ...
   540     // android/apis/content/ReadAsset.java imply that Android's
   541     // AssetInputStream.available() /will/ always return the total file size
   542 
   543     // size = inputStream.available();
   544     mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
   545             "available", "()I");
   546     ctx->hidden.androidio.size = mEnv->CallIntMethod(inputStream, mid);
   547     if (Android_JNI_ExceptionOccurred()) {
   548         goto failure;
   549     }
   550 
   551     // readableByteChannel = Channels.newChannel(inputStream);
   552     channels = mEnv->FindClass("java/nio/channels/Channels");
   553     mid = mEnv->GetStaticMethodID(channels,
   554             "newChannel",
   555             "(Ljava/io/InputStream;)Ljava/nio/channels/ReadableByteChannel;");
   556     readableByteChannel = mEnv->CallStaticObjectMethod(
   557             channels, mid, inputStream);
   558     if (Android_JNI_ExceptionOccurred()) {
   559         goto failure;
   560     }
   561 
   562     ctx->hidden.androidio.readableByteChannelRef =
   563         mEnv->NewGlobalRef(readableByteChannel);
   564 
   565     // Store .read id for reading purposes
   566     mid = mEnv->GetMethodID(mEnv->GetObjectClass(readableByteChannel),
   567             "read", "(Ljava/nio/ByteBuffer;)I");
   568     ctx->hidden.androidio.readMethod = mid;
   569 
   570     ctx->hidden.androidio.position = 0;
   571 
   572     if (false) {
   573 failure:
   574         result = -1;
   575 
   576         mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.fileNameRef);
   577 
   578         if(ctx->hidden.androidio.inputStreamRef != NULL) {
   579             mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.inputStreamRef);
   580         }
   581 
   582         if(ctx->hidden.androidio.readableByteChannelRef != NULL) {
   583             mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.readableByteChannelRef);
   584         }
   585 
   586     }
   587 
   588     return result;
   589 }
   590 
   591 extern "C" int Android_JNI_FileOpen(SDL_RWops* ctx,
   592         const char* fileName, const char*)
   593 {
   594     LocalReferenceHolder refs;
   595     JNIEnv *mEnv = Android_JNI_GetEnv();
   596 
   597     if (!refs.init(mEnv)) {
   598         return -1;
   599     }
   600 
   601     if (!ctx) {
   602         return -1;
   603     }
   604 
   605     jstring fileNameJString = mEnv->NewStringUTF(fileName);
   606     ctx->hidden.androidio.fileNameRef = mEnv->NewGlobalRef(fileNameJString);
   607     ctx->hidden.androidio.inputStreamRef = NULL;
   608     ctx->hidden.androidio.readableByteChannelRef = NULL;
   609     ctx->hidden.androidio.readMethod = NULL;
   610 
   611     return Android_JNI_FileOpen(ctx);
   612 }
   613 
   614 extern "C" size_t Android_JNI_FileRead(SDL_RWops* ctx, void* buffer,
   615         size_t size, size_t maxnum)
   616 {
   617     LocalReferenceHolder refs;
   618     jlong bytesRemaining = (jlong) (size * maxnum);
   619     jlong bytesMax = (jlong) (ctx->hidden.androidio.size -  ctx->hidden.androidio.position);
   620     int bytesRead = 0;
   621 
   622     /* Don't read more bytes than those that remain in the file, otherwise we get an exception */
   623     if (bytesRemaining >  bytesMax) bytesRemaining = bytesMax;
   624 
   625     JNIEnv *mEnv = Android_JNI_GetEnv();
   626     if (!refs.init(mEnv)) {
   627         return -1;
   628     }
   629 
   630     jobject readableByteChannel = (jobject)ctx->hidden.androidio.readableByteChannelRef;
   631     jmethodID readMethod = (jmethodID)ctx->hidden.androidio.readMethod;
   632     jobject byteBuffer = mEnv->NewDirectByteBuffer(buffer, bytesRemaining);
   633 
   634     while (bytesRemaining > 0) {
   635         // result = readableByteChannel.read(...);
   636         int result = mEnv->CallIntMethod(readableByteChannel, readMethod, byteBuffer);
   637 
   638         if (Android_JNI_ExceptionOccurred()) {
   639             return 0;
   640         }
   641 
   642         if (result < 0) {
   643             break;
   644         }
   645 
   646         bytesRemaining -= result;
   647         bytesRead += result;
   648         ctx->hidden.androidio.position += result;
   649     }
   650 
   651     return bytesRead / size;
   652 }
   653 
   654 extern "C" size_t Android_JNI_FileWrite(SDL_RWops* ctx, const void* buffer,
   655         size_t size, size_t num)
   656 {
   657     SDL_SetError("Cannot write to Android package filesystem");
   658     return 0;
   659 }
   660 
   661 static int Android_JNI_FileClose(SDL_RWops* ctx, bool release)
   662 {
   663     LocalReferenceHolder refs;
   664     int result = 0;
   665     JNIEnv *mEnv = Android_JNI_GetEnv();
   666 
   667     if (!refs.init(mEnv)) {
   668         SDL_SetError("Failed to allocate enough JVM local references");
   669         return -1;
   670     }
   671 
   672     if (ctx) {
   673         if (release) {
   674             mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.fileNameRef);
   675         }
   676 
   677         jobject inputStream = (jobject)ctx->hidden.androidio.inputStreamRef;
   678 
   679         // inputStream.close();
   680         jmethodID mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
   681                 "close", "()V");
   682         mEnv->CallVoidMethod(inputStream, mid);
   683         mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.inputStreamRef);
   684         mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.readableByteChannelRef);
   685         if (Android_JNI_ExceptionOccurred()) {
   686             result = -1;
   687         }
   688 
   689         if (release) {
   690             SDL_FreeRW(ctx);
   691         }
   692     }
   693 
   694     return result;
   695 }
   696 
   697 
   698 extern "C" long Android_JNI_FileSeek(SDL_RWops* ctx, long offset, int whence)
   699 {
   700     long newPosition;
   701 
   702     switch (whence) {
   703         case RW_SEEK_SET:
   704             newPosition = offset;
   705             break;
   706         case RW_SEEK_CUR:
   707             newPosition = ctx->hidden.androidio.position + offset;
   708             break;
   709         case RW_SEEK_END:
   710             newPosition = ctx->hidden.androidio.size + offset;
   711             break;
   712         default:
   713             SDL_SetError("Unknown value for 'whence'");
   714             return -1;
   715     }
   716     if (newPosition < 0) {
   717         newPosition = 0;
   718     }
   719     if (newPosition > ctx->hidden.androidio.size) {
   720         newPosition = ctx->hidden.androidio.size;
   721     }
   722 
   723     long movement = newPosition - ctx->hidden.androidio.position;
   724     jobject inputStream = (jobject)ctx->hidden.androidio.inputStreamRef;
   725 
   726     if (movement > 0) {
   727         unsigned char buffer[1024];
   728 
   729         // The easy case where we're seeking forwards
   730         while (movement > 0) {
   731             long amount = (long) sizeof (buffer);
   732             if (amount > movement) {
   733                 amount = movement;
   734             }
   735             size_t result = Android_JNI_FileRead(ctx, buffer, 1, amount);
   736 
   737             if (result <= 0) {
   738                 // Failed to read/skip the required amount, so fail
   739                 return -1;
   740             }
   741 
   742             movement -= result;
   743         }
   744     } else if (movement < 0) {
   745         // We can't seek backwards so we have to reopen the file and seek
   746         // forwards which obviously isn't very efficient
   747         Android_JNI_FileClose(ctx, false);
   748         Android_JNI_FileOpen(ctx);
   749         Android_JNI_FileSeek(ctx, newPosition, RW_SEEK_SET);
   750     }
   751 
   752     ctx->hidden.androidio.position = newPosition;
   753 
   754     return ctx->hidden.androidio.position;
   755 }
   756 
   757 extern "C" int Android_JNI_FileClose(SDL_RWops* ctx)
   758 {
   759     return Android_JNI_FileClose(ctx, true);
   760 }
   761 
   762 // returns a new global reference which needs to be released later
   763 static jobject Android_JNI_GetSystemServiceObject(const char* name)
   764 {
   765     LocalReferenceHolder refs;
   766     JNIEnv* env = Android_JNI_GetEnv();
   767     if (!refs.init(env)) {
   768         return NULL;
   769     }
   770 
   771     jstring service = env->NewStringUTF(name);
   772 
   773     jmethodID mid;
   774 
   775     mid = env->GetStaticMethodID(mActivityClass, "getContext", "()Landroid/content/Context;");
   776     jobject context = env->CallStaticObjectMethod(mActivityClass, mid);
   777 
   778     mid = env->GetMethodID(mActivityClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");
   779     jobject manager = env->CallObjectMethod(context, mid, service);
   780 
   781     env->DeleteLocalRef(service);
   782 
   783     return manager ? env->NewGlobalRef(manager) : NULL;
   784 }
   785 
   786 #define SETUP_CLIPBOARD(error) \
   787     LocalReferenceHolder refs; \
   788     JNIEnv* env = Android_JNI_GetEnv(); \
   789     if (!refs.init(env)) { \
   790         return error; \
   791     } \
   792     jobject clipboard = Android_JNI_GetSystemServiceObject("clipboard"); \
   793     if (!clipboard) { \
   794         return error; \
   795     }
   796 
   797 extern "C" int Android_JNI_SetClipboardText(const char* text)
   798 {
   799     SETUP_CLIPBOARD(-1)
   800 
   801     jmethodID mid = env->GetMethodID(env->GetObjectClass(clipboard), "setText", "(Ljava/lang/CharSequence;)V");
   802     jstring string = env->NewStringUTF(text);
   803     env->CallVoidMethod(clipboard, mid, string);
   804     env->DeleteGlobalRef(clipboard);
   805     env->DeleteLocalRef(string);
   806     return 0;
   807 }
   808 
   809 extern "C" char* Android_JNI_GetClipboardText()
   810 {
   811     SETUP_CLIPBOARD(SDL_strdup(""))
   812 
   813     jmethodID mid = env->GetMethodID(env->GetObjectClass(clipboard), "getText", "()Ljava/lang/CharSequence;");
   814     jobject sequence = env->CallObjectMethod(clipboard, mid);
   815     env->DeleteGlobalRef(clipboard);
   816     if (sequence) {
   817         mid = env->GetMethodID(env->GetObjectClass(sequence), "toString", "()Ljava/lang/String;");
   818         jstring string = reinterpret_cast<jstring>(env->CallObjectMethod(sequence, mid));
   819         const char* utf = env->GetStringUTFChars(string, 0);
   820         if (utf) {
   821             char* text = SDL_strdup(utf);
   822             env->ReleaseStringUTFChars(string, utf);
   823             return text;
   824         }
   825     }
   826     return SDL_strdup("");
   827 }
   828 
   829 extern "C" SDL_bool Android_JNI_HasClipboardText()
   830 {
   831     SETUP_CLIPBOARD(SDL_FALSE)
   832 
   833     jmethodID mid = env->GetMethodID(env->GetObjectClass(clipboard), "hasText", "()Z");
   834     jboolean has = env->CallBooleanMethod(clipboard, mid);
   835     env->DeleteGlobalRef(clipboard);
   836     return has ? SDL_TRUE : SDL_FALSE;
   837 }
   838 
   839 
   840 // returns 0 on success or -1 on error (others undefined then)
   841 // returns truthy or falsy value in plugged, charged and battery
   842 // returns the value in seconds and percent or -1 if not available
   843 extern "C" int Android_JNI_GetPowerInfo(int* plugged, int* charged, int* battery, int* seconds, int* percent)
   844 {
   845     LocalReferenceHolder refs;
   846     JNIEnv* env = Android_JNI_GetEnv();
   847     if (!refs.init(env)) {
   848         return -1;
   849     }
   850 
   851     jmethodID mid;
   852 
   853     mid = env->GetStaticMethodID(mActivityClass, "getContext", "()Landroid/content/Context;");
   854     jobject context = env->CallStaticObjectMethod(mActivityClass, mid);
   855 
   856     jstring action = env->NewStringUTF("android.intent.action.BATTERY_CHANGED");
   857 
   858     jclass cls = env->FindClass("android/content/IntentFilter");
   859 
   860     mid = env->GetMethodID(cls, "<init>", "(Ljava/lang/String;)V");
   861     jobject filter = env->NewObject(cls, mid, action);
   862 
   863     env->DeleteLocalRef(action);
   864 
   865     mid = env->GetMethodID(mActivityClass, "registerReceiver", "(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent;");
   866     jobject intent = env->CallObjectMethod(context, mid, NULL, filter);
   867 
   868     env->DeleteLocalRef(filter);
   869 
   870     cls = env->GetObjectClass(intent);
   871 
   872     jstring iname;
   873     jmethodID imid = env->GetMethodID(cls, "getIntExtra", "(Ljava/lang/String;I)I");
   874 
   875 #define GET_INT_EXTRA(var, key) \
   876     iname = env->NewStringUTF(key); \
   877     int var = env->CallIntMethod(intent, imid, iname, -1); \
   878     env->DeleteLocalRef(iname);
   879 
   880     jstring bname;
   881     jmethodID bmid = env->GetMethodID(cls, "getBooleanExtra", "(Ljava/lang/String;Z)Z");
   882 
   883 #define GET_BOOL_EXTRA(var, key) \
   884     bname = env->NewStringUTF(key); \
   885     int var = env->CallBooleanMethod(intent, bmid, bname, JNI_FALSE); \
   886     env->DeleteLocalRef(bname);
   887 
   888     if (plugged) {
   889         GET_INT_EXTRA(plug, "plugged") // == BatteryManager.EXTRA_PLUGGED (API 5)
   890         if (plug == -1) {
   891             return -1;
   892         }
   893         // 1 == BatteryManager.BATTERY_PLUGGED_AC
   894         // 2 == BatteryManager.BATTERY_PLUGGED_USB
   895         *plugged = (0 < plug) ? 1 : 0;
   896     }
   897 
   898     if (charged) {
   899         GET_INT_EXTRA(status, "status") // == BatteryManager.EXTRA_STATUS (API 5)
   900         if (status == -1) {
   901             return -1;
   902         }
   903         // 5 == BatteryManager.BATTERY_STATUS_FULL
   904         *charged = (status == 5) ? 1 : 0;
   905     }
   906 
   907     if (battery) {
   908         GET_BOOL_EXTRA(present, "present") // == BatteryManager.EXTRA_PRESENT (API 5)
   909         *battery = present ? 1 : 0;
   910     }
   911 
   912     if (seconds) {
   913         *seconds = -1; // not possible
   914     }
   915 
   916     if (percent) {
   917         GET_INT_EXTRA(level, "level") // == BatteryManager.EXTRA_LEVEL (API 5)
   918         GET_INT_EXTRA(scale, "scale") // == BatteryManager.EXTRA_SCALE (API 5)
   919         if ((level == -1) || (scale == -1)) {
   920             return -1;
   921         }
   922         *percent = level * 100 / scale;
   923     }
   924 
   925     env->DeleteLocalRef(intent);
   926 
   927     return 0;
   928 }
   929 
   930 // sends message to be handled on the UI event dispatch thread
   931 extern "C" int Android_JNI_SendMessage(int command, int param)
   932 {
   933     JNIEnv *env = Android_JNI_GetEnv();
   934     if (!env) {
   935         return -1;
   936     }
   937     jmethodID mid = env->GetStaticMethodID(mActivityClass, "sendMessage", "(II)V");
   938     if (!mid) {
   939         return -1;
   940     }
   941     env->CallStaticVoidMethod(mActivityClass, mid, command, param);
   942     return 0;
   943 }
   944 
   945 extern "C" int Android_JNI_ShowTextInput(SDL_Rect *inputRect)
   946 {
   947     JNIEnv *env = Android_JNI_GetEnv();
   948     if (!env) {
   949         return -1;
   950     }
   951 
   952     jmethodID mid = env->GetStaticMethodID(mActivityClass, "showTextInput", "(IIII)V");
   953     if (!mid) {
   954         return -1;
   955     }
   956     env->CallStaticVoidMethod( mActivityClass, mid,
   957                                inputRect->x,
   958                                inputRect->y,
   959                                inputRect->w,
   960                                inputRect->h );
   961     return 0;
   962 }
   963 
   964 /*extern "C" int Android_JNI_HideTextInput()
   965 {
   966 
   967 
   968     JNIEnv *env = Android_JNI_GetEnv();
   969     if (!env) {
   970         return -1;
   971     }
   972 
   973     jmethodID mid = env->GetStaticMethodID(mActivityClass, "hideTextInput", "()V");
   974     if (!mid) {
   975         return -1;
   976     }
   977     env->CallStaticVoidMethod(mActivityClass, mid);
   978     return 0;
   979 }*/
   980 
   981 #endif /* __ANDROID__ */
   982 
   983 /* vi: set ts=4 sw=4 expandtab: */