Fixes Android_JNI_FileRead behaviour where reading past the end of a file returns zero instead of the number of bytes read.
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2012 Sam Lantinga <slouken@libsdl.org>
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.
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:
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.
21 #include "SDL_config.h"
22 #include "SDL_stdinc.h"
23 #include "SDL_assert.h"
27 #include "SDL_android.h"
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"
35 #include <android/log.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)
44 /* Implemented in audio/android/SDL_androidaudio.c */
45 extern void Android_RunAudioThread();
48 /*******************************************************************************
49 This file links the Java side of Android with libsdl
50 *******************************************************************************/
52 #include <android/log.h>
55 /*******************************************************************************
57 *******************************************************************************/
58 static pthread_key_t mThreadKey;
59 static JavaVM* mJavaVM;
62 static jclass mActivityClass;
65 static jmethodID midCreateGLContext;
66 static jmethodID midFlipBuffers;
67 static jmethodID midAudioInit;
68 static jmethodID midAudioWriteShortBuffer;
69 static jmethodID midAudioWriteByteBuffer;
70 static jmethodID midAudioQuit;
72 // Accelerometer data storage
73 static float fLastAccelerometer[3];
74 static bool bHasNewData;
76 /*******************************************************************************
77 Functions called by JNI
78 *******************************************************************************/
81 extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
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()");
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
94 if (pthread_key_create(&mThreadKey, Android_JNI_ThreadDestroyed)) {
95 __android_log_print(ANDROID_LOG_ERROR, "SDL", "Error initializing pthread key");
98 Android_JNI_SetupThread();
101 return JNI_VERSION_1_4;
104 // Called before SDL_main() to initialize JNI bindings
105 extern "C" void SDL_Android_Init(JNIEnv* mEnv, jclass cls)
107 __android_log_print(ANDROID_LOG_INFO, "SDL", "SDL_Android_Init()");
109 Android_JNI_SetupThread();
111 mActivityClass = (jclass)mEnv->NewGlobalRef(cls);
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,
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");
132 __android_log_print(ANDROID_LOG_INFO, "SDL", "SDL_Android_Init() finished!");
136 extern "C" void Java_org_libsdl_app_SDLActivity_onNativeResize(
137 JNIEnv* env, jclass jcls,
138 jint width, jint height, jint format)
140 Android_SetScreenResolution(width, height, format);
144 extern "C" void Java_org_libsdl_app_SDLActivity_onNativeKeyDown(
145 JNIEnv* env, jclass jcls, jint keycode)
147 Android_OnKeyDown(keycode);
151 extern "C" void Java_org_libsdl_app_SDLActivity_onNativeKeyUp(
152 JNIEnv* env, jclass jcls, jint keycode)
154 Android_OnKeyUp(keycode);
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)
163 Android_OnTouch(touch_device_id_in, pointer_finger_id_in, action, x, y, p);
167 extern "C" void Java_org_libsdl_app_SDLActivity_onNativeAccel(
168 JNIEnv* env, jclass jcls,
169 jfloat x, jfloat y, jfloat z)
171 fLastAccelerometer[0] = x;
172 fLastAccelerometer[1] = y;
173 fLastAccelerometer[2] = z;
178 extern "C" void Java_org_libsdl_app_SDLActivity_nativeQuit(
179 JNIEnv* env, jclass cls)
181 // Inject a SDL_QUIT event
186 extern "C" void Java_org_libsdl_app_SDLActivity_nativePause(
187 JNIEnv* env, jclass cls)
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);
198 extern "C" void Java_org_libsdl_app_SDLActivity_nativeResume(
199 JNIEnv* env, jclass cls)
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.
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);
212 extern "C" void Java_org_libsdl_app_SDLActivity_nativeRunAudioThread(
213 JNIEnv* env, jclass cls)
215 /* This is the audio thread, with a different environment */
216 Android_JNI_SetupThread();
218 Android_RunAudioThread();
222 /*******************************************************************************
223 Functions called by SDL into Java
224 *******************************************************************************/
226 class LocalReferenceHolder
232 static bool IsActive() {
237 LocalReferenceHolder() : m_env(NULL) { }
238 ~LocalReferenceHolder() {
240 m_env->PopLocalFrame(NULL);
245 bool init(JNIEnv *env, jint capacity = 16) {
246 if (env->PushLocalFrame(capacity) < 0) {
247 SDL_SetError("Failed to allocate enough JVM local references");
258 int LocalReferenceHolder::s_active;
260 extern "C" SDL_bool Android_JNI_CreateContext(int majorVersion, int minorVersion)
262 JNIEnv *mEnv = Android_JNI_GetEnv();
263 if (mEnv->CallStaticBooleanMethod(mActivityClass, midCreateGLContext, majorVersion, minorVersion)) {
270 extern "C" void Android_JNI_SwapWindow()
272 JNIEnv *mEnv = Android_JNI_GetEnv();
273 mEnv->CallStaticVoidMethod(mActivityClass, midFlipBuffers);
276 extern "C" void Android_JNI_SetActivityTitle(const char *title)
279 JNIEnv *mEnv = Android_JNI_GetEnv();
280 mid = mEnv->GetStaticMethodID(mActivityClass,"setActivityTitle","(Ljava/lang/String;)V");
282 jstring jtitle = reinterpret_cast<jstring>(mEnv->NewStringUTF(title));
283 mEnv->CallStaticVoidMethod(mActivityClass, mid, jtitle);
284 mEnv->DeleteLocalRef(jtitle);
288 extern "C" SDL_bool Android_JNI_GetAccelerometerValues(float values[3])
291 SDL_bool retval = SDL_FALSE;
294 for (i = 0; i < 3; ++i) {
295 values[i] = fLastAccelerometer[i];
304 static void Android_JNI_ThreadDestroyed(void* value) {
305 /* The thread is being destroyed, detach it from the Java VM and set the mThreadKey value to NULL as required */
306 JNIEnv *env = (JNIEnv*) value;
308 mJavaVM->DetachCurrentThread();
309 pthread_setspecific(mThreadKey, NULL);
313 JNIEnv* Android_JNI_GetEnv(void) {
314 /* From http://developer.android.com/guide/practices/jni.html
315 * All threads are Linux threads, scheduled by the kernel.
316 * They're usually started from managed code (using Thread.start), but they can also be created elsewhere and then
317 * attached to the JavaVM. For example, a thread started with pthread_create can be attached with the
318 * JNI AttachCurrentThread or AttachCurrentThreadAsDaemon functions. Until a thread is attached, it has no JNIEnv,
319 * and cannot make JNI calls.
320 * Attaching a natively-created thread causes a java.lang.Thread object to be constructed and added to the "main"
321 * ThreadGroup, making it visible to the debugger. Calling AttachCurrentThread on an already-attached thread
323 * Note: You can call this function any number of times for the same thread, there's no harm in it
327 int status = mJavaVM->AttachCurrentThread(&env, NULL);
329 LOGE("failed to attach current thread");
336 int Android_JNI_SetupThread(void) {
337 /* From http://developer.android.com/guide/practices/jni.html
338 * Threads attached through JNI must call DetachCurrentThread before they exit. If coding this directly is awkward,
339 * in Android 2.0 (Eclair) and higher you can use pthread_key_create to define a destructor function that will be
340 * called before the thread exits, and call DetachCurrentThread from there. (Use that key with pthread_setspecific
341 * to store the JNIEnv in thread-local-storage; that way it'll be passed into your destructor as the argument.)
342 * Note: The destructor is not called unless the stored value is != NULL
343 * Note: You can call this function any number of times for the same thread, there's no harm in it
344 * (except for some lost CPU cycles)
346 JNIEnv *env = Android_JNI_GetEnv();
347 pthread_setspecific(mThreadKey, (void*) env);
354 static jboolean audioBuffer16Bit = JNI_FALSE;
355 static jboolean audioBufferStereo = JNI_FALSE;
356 static jobject audioBuffer = NULL;
357 static void* audioBufferPinned = NULL;
359 extern "C" int Android_JNI_OpenAudioDevice(int sampleRate, int is16Bit, int channelCount, int desiredBufferFrames)
361 int audioBufferFrames;
364 JNIEnv *env = Android_JNI_GetEnv();
367 LOGE("callback_handler: failed to attach current thread");
369 Android_JNI_SetupThread();
372 __android_log_print(ANDROID_LOG_VERBOSE, "SDL", "SDL audio: opening device");
373 audioBuffer16Bit = is16Bit;
374 audioBufferStereo = channelCount > 1;
376 audioBuffer = env->CallStaticObjectMethod(mActivityClass, midAudioInit, sampleRate, audioBuffer16Bit, audioBufferStereo, desiredBufferFrames);
378 if (audioBuffer == NULL) {
379 __android_log_print(ANDROID_LOG_WARN, "SDL", "SDL audio: didn't get back a good audio buffer!");
382 audioBuffer = env->NewGlobalRef(audioBuffer);
384 jboolean isCopy = JNI_FALSE;
385 if (audioBuffer16Bit) {
386 audioBufferPinned = env->GetShortArrayElements((jshortArray)audioBuffer, &isCopy);
387 audioBufferFrames = env->GetArrayLength((jshortArray)audioBuffer);
389 audioBufferPinned = env->GetByteArrayElements((jbyteArray)audioBuffer, &isCopy);
390 audioBufferFrames = env->GetArrayLength((jbyteArray)audioBuffer);
392 if (audioBufferStereo) {
393 audioBufferFrames /= 2;
396 return audioBufferFrames;
399 extern "C" void * Android_JNI_GetAudioBuffer()
401 return audioBufferPinned;
404 extern "C" void Android_JNI_WriteAudioBuffer()
406 JNIEnv *mAudioEnv = Android_JNI_GetEnv();
408 if (audioBuffer16Bit) {
409 mAudioEnv->ReleaseShortArrayElements((jshortArray)audioBuffer, (jshort *)audioBufferPinned, JNI_COMMIT);
410 mAudioEnv->CallStaticVoidMethod(mActivityClass, midAudioWriteShortBuffer, (jshortArray)audioBuffer);
412 mAudioEnv->ReleaseByteArrayElements((jbyteArray)audioBuffer, (jbyte *)audioBufferPinned, JNI_COMMIT);
413 mAudioEnv->CallStaticVoidMethod(mActivityClass, midAudioWriteByteBuffer, (jbyteArray)audioBuffer);
416 /* JNI_COMMIT means the changes are committed to the VM but the buffer remains pinned */
419 extern "C" void Android_JNI_CloseAudioDevice()
422 JNIEnv *env = Android_JNI_GetEnv();
424 env->CallStaticVoidMethod(mActivityClass, midAudioQuit);
427 env->DeleteGlobalRef(audioBuffer);
429 audioBufferPinned = NULL;
433 // Test for an exception and call SDL_SetError with its detail if one occurs
434 static bool Android_JNI_ExceptionOccurred()
436 SDL_assert(LocalReferenceHolder::IsActive());
437 JNIEnv *mEnv = Android_JNI_GetEnv();
439 jthrowable exception = mEnv->ExceptionOccurred();
440 if (exception != NULL) {
443 // Until this happens most JNI operations have undefined behaviour
444 mEnv->ExceptionClear();
446 jclass exceptionClass = mEnv->GetObjectClass(exception);
447 jclass classClass = mEnv->FindClass("java/lang/Class");
449 mid = mEnv->GetMethodID(classClass, "getName", "()Ljava/lang/String;");
450 jstring exceptionName = (jstring)mEnv->CallObjectMethod(exceptionClass, mid);
451 const char* exceptionNameUTF8 = mEnv->GetStringUTFChars(exceptionName, 0);
453 mid = mEnv->GetMethodID(exceptionClass, "getMessage", "()Ljava/lang/String;");
454 jstring exceptionMessage = (jstring)mEnv->CallObjectMethod(exception, mid);
456 if (exceptionMessage != NULL) {
457 const char* exceptionMessageUTF8 = mEnv->GetStringUTFChars(
458 exceptionMessage, 0);
459 SDL_SetError("%s: %s", exceptionNameUTF8, exceptionMessageUTF8);
460 mEnv->ReleaseStringUTFChars(exceptionMessage, exceptionMessageUTF8);
462 SDL_SetError("%s", exceptionNameUTF8);
465 mEnv->ReleaseStringUTFChars(exceptionName, exceptionNameUTF8);
473 static int Android_JNI_FileOpen(SDL_RWops* ctx)
475 LocalReferenceHolder refs;
480 jobject assetManager;
483 jobject readableByteChannel;
484 jstring fileNameJString;
486 JNIEnv *mEnv = Android_JNI_GetEnv();
487 if (!refs.init(mEnv)) {
491 fileNameJString = (jstring)ctx->hidden.androidio.fileNameRef;
493 // context = SDLActivity.getContext();
494 mid = mEnv->GetStaticMethodID(mActivityClass,
495 "getContext","()Landroid/content/Context;");
496 context = mEnv->CallStaticObjectMethod(mActivityClass, mid);
498 // assetManager = context.getAssets();
499 mid = mEnv->GetMethodID(mEnv->GetObjectClass(context),
500 "getAssets", "()Landroid/content/res/AssetManager;");
501 assetManager = mEnv->CallObjectMethod(context, mid);
503 // inputStream = assetManager.open(<filename>);
504 mid = mEnv->GetMethodID(mEnv->GetObjectClass(assetManager),
505 "open", "(Ljava/lang/String;)Ljava/io/InputStream;");
506 inputStream = mEnv->CallObjectMethod(assetManager, mid, fileNameJString);
507 if (Android_JNI_ExceptionOccurred()) {
511 ctx->hidden.androidio.inputStreamRef = mEnv->NewGlobalRef(inputStream);
513 // Despite all the visible documentation on [Asset]InputStream claiming
514 // that the .available() method is not guaranteed to return the entire file
515 // size, comments in <sdk>/samples/<ver>/ApiDemos/src/com/example/ ...
516 // android/apis/content/ReadAsset.java imply that Android's
517 // AssetInputStream.available() /will/ always return the total file size
519 // size = inputStream.available();
520 mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
522 ctx->hidden.androidio.size = mEnv->CallIntMethod(inputStream, mid);
523 if (Android_JNI_ExceptionOccurred()) {
527 // readableByteChannel = Channels.newChannel(inputStream);
528 channels = mEnv->FindClass("java/nio/channels/Channels");
529 mid = mEnv->GetStaticMethodID(channels,
531 "(Ljava/io/InputStream;)Ljava/nio/channels/ReadableByteChannel;");
532 readableByteChannel = mEnv->CallStaticObjectMethod(
533 channels, mid, inputStream);
534 if (Android_JNI_ExceptionOccurred()) {
538 ctx->hidden.androidio.readableByteChannelRef =
539 mEnv->NewGlobalRef(readableByteChannel);
541 // Store .read id for reading purposes
542 mid = mEnv->GetMethodID(mEnv->GetObjectClass(readableByteChannel),
543 "read", "(Ljava/nio/ByteBuffer;)I");
544 ctx->hidden.androidio.readMethod = mid;
546 ctx->hidden.androidio.position = 0;
552 mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.fileNameRef);
554 if(ctx->hidden.androidio.inputStreamRef != NULL) {
555 mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.inputStreamRef);
558 if(ctx->hidden.androidio.readableByteChannelRef != NULL) {
559 mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.readableByteChannelRef);
567 extern "C" int Android_JNI_FileOpen(SDL_RWops* ctx,
568 const char* fileName, const char*)
570 LocalReferenceHolder refs;
571 JNIEnv *mEnv = Android_JNI_GetEnv();
573 if (!refs.init(mEnv)) {
581 jstring fileNameJString = mEnv->NewStringUTF(fileName);
582 ctx->hidden.androidio.fileNameRef = mEnv->NewGlobalRef(fileNameJString);
583 ctx->hidden.androidio.inputStreamRef = NULL;
584 ctx->hidden.androidio.readableByteChannelRef = NULL;
585 ctx->hidden.androidio.readMethod = NULL;
587 return Android_JNI_FileOpen(ctx);
590 extern "C" size_t Android_JNI_FileRead(SDL_RWops* ctx, void* buffer,
591 size_t size, size_t maxnum)
593 LocalReferenceHolder refs;
594 jlong bytesRemaining = (jlong) (size * maxnum);
595 jlong bytesMax = (jlong) (ctx->hidden.androidio.size - ctx->hidden.androidio.position);
598 /* Don't read more bytes than those that remain in the file, otherwise we get an exception */
599 if (bytesRemaining > bytesMax) bytesRemaining = bytesMax;
601 JNIEnv *mEnv = Android_JNI_GetEnv();
602 if (!refs.init(mEnv)) {
606 jobject readableByteChannel = (jobject)ctx->hidden.androidio.readableByteChannelRef;
607 jmethodID readMethod = (jmethodID)ctx->hidden.androidio.readMethod;
608 jobject byteBuffer = mEnv->NewDirectByteBuffer(buffer, bytesRemaining);
610 while (bytesRemaining > 0) {
611 // result = readableByteChannel.read(...);
612 int result = mEnv->CallIntMethod(readableByteChannel, readMethod, byteBuffer);
614 if (Android_JNI_ExceptionOccurred()) {
622 bytesRemaining -= result;
624 ctx->hidden.androidio.position += result;
627 return bytesRead / size;
630 extern "C" size_t Android_JNI_FileWrite(SDL_RWops* ctx, const void* buffer,
631 size_t size, size_t num)
633 SDL_SetError("Cannot write to Android package filesystem");
637 static int Android_JNI_FileClose(SDL_RWops* ctx, bool release)
639 LocalReferenceHolder refs;
641 JNIEnv *mEnv = Android_JNI_GetEnv();
643 if (!refs.init(mEnv)) {
644 SDL_SetError("Failed to allocate enough JVM local references");
650 mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.fileNameRef);
653 jobject inputStream = (jobject)ctx->hidden.androidio.inputStreamRef;
655 // inputStream.close();
656 jmethodID mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
658 mEnv->CallVoidMethod(inputStream, mid);
659 mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.inputStreamRef);
660 mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.readableByteChannelRef);
661 if (Android_JNI_ExceptionOccurred()) {
674 extern "C" long Android_JNI_FileSeek(SDL_RWops* ctx, long offset, int whence)
680 newPosition = offset;
683 newPosition = ctx->hidden.androidio.position + offset;
686 newPosition = ctx->hidden.androidio.size + offset;
689 SDL_SetError("Unknown value for 'whence'");
692 if (newPosition < 0) {
695 if (newPosition > ctx->hidden.androidio.size) {
696 newPosition = ctx->hidden.androidio.size;
699 long movement = newPosition - ctx->hidden.androidio.position;
700 jobject inputStream = (jobject)ctx->hidden.androidio.inputStreamRef;
703 unsigned char buffer[1024];
705 // The easy case where we're seeking forwards
706 while (movement > 0) {
707 long amount = (long) sizeof (buffer);
708 if (amount > movement) {
711 size_t result = Android_JNI_FileRead(ctx, buffer, 1, amount);
714 // Failed to read/skip the required amount, so fail
720 } else if (movement < 0) {
721 // We can't seek backwards so we have to reopen the file and seek
722 // forwards which obviously isn't very efficient
723 Android_JNI_FileClose(ctx, false);
724 Android_JNI_FileOpen(ctx);
725 Android_JNI_FileSeek(ctx, newPosition, RW_SEEK_SET);
728 ctx->hidden.androidio.position = newPosition;
730 return ctx->hidden.androidio.position;
733 extern "C" int Android_JNI_FileClose(SDL_RWops* ctx)
735 return Android_JNI_FileClose(ctx, true);
738 #endif /* __ANDROID__ */
740 /* vi: set ts=4 sw=4 expandtab: */