Allow Android platforms to read from .apk files via the RWOPS interface.
authorRyan C. Gordon <icculus@icculus.org>
Fri, 29 Jul 2011 16:51:25 -0400
changeset 5582 1281a3f1f0a6
parent 5581 f40f9d3ca2bc
child 5583 68eb59d8baed
Allow Android platforms to read from .apk files via the RWOPS interface. Fixes Bugzilla #1261. Thanks to Tim Angus for the patch!
android-project/src/org/libsdl/app/SDLActivity.java
include/SDL_rwops.h
src/core/android/SDL_android.cpp
src/core/android/SDL_android.h
src/file/SDL_rwops.c
--- a/android-project/src/org/libsdl/app/SDLActivity.java	Wed Jul 27 18:07:40 2011 -0400
+++ b/android-project/src/org/libsdl/app/SDLActivity.java	Fri Jul 29 16:51:25 2011 -0400
@@ -114,6 +114,10 @@
         mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title);
     }
 
+	public static Context getContext() {
+		return mSingleton;
+	}
+
     // Audio
     private static Object buf;
     
--- a/include/SDL_rwops.h	Wed Jul 27 18:07:40 2011 -0400
+++ b/include/SDL_rwops.h	Fri Jul 29 16:51:25 2011 -0400
@@ -82,7 +82,21 @@
     Uint32 type;
     union
     {
-#ifdef __WIN32__
+#if defined(ANDROID)
+        struct
+        {
+            void *fileName;
+            void *fileNameRef;
+            void *inputStream;
+            void *inputStreamRef;
+            void *skipMethod;
+            void *readableByteChannel;
+            void *readableByteChannelRef;
+            void *readMethod;
+            long position;
+            int size;
+        } androidio;
+#elif defined(__WIN32__)
         struct
         {
             SDL_bool append;
@@ -95,6 +109,7 @@
             } buffer;
         } windowsio;
 #endif
+
 #ifdef HAVE_STDIO_H
         struct
         {
--- a/src/core/android/SDL_android.cpp	Wed Jul 27 18:07:40 2011 -0400
+++ b/src/core/android/SDL_android.cpp	Fri Jul 29 16:51:25 2011 -0400
@@ -259,4 +259,234 @@
     }
 }
 
+static int Android_JNI_FileOpen(SDL_RWops* ctx)
+{
+    jstring fileNameJString = (jstring)ctx->hidden.androidio.fileName;
+
+    // context = SDLActivity.getContext();
+    jmethodID mid = mEnv->GetStaticMethodID(mActivityClass,
+            "getContext","()Landroid/content/Context;");
+    jobject context = mEnv->CallStaticObjectMethod(mActivityClass, mid);
+
+    // assetManager = context.getAssets();
+    mid = mEnv->GetMethodID(mEnv->GetObjectClass(context),
+            "getAssets","()Landroid/content/res/AssetManager;");
+    jobject assetManager = mEnv->CallObjectMethod(context, mid);
+
+    // inputStream = assetManager.open(<filename>);
+    mEnv->ExceptionClear();
+    mid = mEnv->GetMethodID(mEnv->GetObjectClass(assetManager),
+            "open", "(Ljava/lang/String;)Ljava/io/InputStream;");
+    jobject inputStream = mEnv->CallObjectMethod(assetManager, mid, fileNameJString);
+    if (mEnv->ExceptionOccurred()) {
+        mEnv->ExceptionDescribe();
+        mEnv->ExceptionClear();
+        mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.fileNameRef);
+        return -1;
+    } else {
+        ctx->hidden.androidio.inputStream = inputStream;
+        ctx->hidden.androidio.inputStreamRef = mEnv->NewGlobalRef(inputStream);
+    }
+
+    // Store .skip id for seeking purposes
+    mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
+            "skip", "(J)J");
+    ctx->hidden.androidio.skipMethod = mid;
+
+    // Despite all the visible documentation on [Asset]InputStream claiming
+    // that the .available() method is not guaranteed to return the entire file
+    // size, comments in <sdk>/samples/<ver>/ApiDemos/src/com/example/ ...
+    // android/apis/content/ReadAsset.java imply that Android's
+    // AssetInputStream.available() /will/ always return the total file size
+
+    // size = inputStream.available();
+    mEnv->ExceptionClear();
+    mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
+            "available", "()I");
+    ctx->hidden.androidio.size = mEnv->CallIntMethod(inputStream, mid);
+    if (mEnv->ExceptionOccurred()) {
+        mEnv->ExceptionDescribe();
+        mEnv->ExceptionClear();
+        mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.fileNameRef);
+        mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.inputStreamRef);
+        return -1;
+    }
+
+    // readableByteChannel = Channels.newChannel(inputStream);
+    mEnv->ExceptionClear();
+    jclass channels = mEnv->FindClass("java/nio/channels/Channels");
+    mid = mEnv->GetStaticMethodID(channels,
+            "newChannel",
+            "(Ljava/io/InputStream;)Ljava/nio/channels/ReadableByteChannel;");
+    jobject readableByteChannel = mEnv->CallStaticObjectMethod(
+            channels, mid, inputStream);
+    if (mEnv->ExceptionOccurred()) {
+        mEnv->ExceptionDescribe();
+        mEnv->ExceptionClear();
+        mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.fileNameRef);
+        mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.inputStreamRef);
+        return -1;
+    } else {
+        ctx->hidden.androidio.readableByteChannel = readableByteChannel;
+        ctx->hidden.androidio.readableByteChannelRef =
+            mEnv->NewGlobalRef(readableByteChannel);
+    }
+
+    // Store .read id for reading purposes
+    mid = mEnv->GetMethodID(mEnv->GetObjectClass(readableByteChannel),
+            "read", "(Ljava/nio/ByteBuffer;)I");
+    ctx->hidden.androidio.readMethod = mid;
+
+    ctx->hidden.androidio.position = 0;
+
+    return 0;
+}
+
+extern "C" int Android_JNI_FileOpen(SDL_RWops* ctx,
+        const char* fileName, const char*)
+{
+    if (!ctx) {
+        return -1;
+    }
+
+    jstring fileNameJString = mEnv->NewStringUTF(fileName);
+    ctx->hidden.androidio.fileName = fileNameJString;
+    ctx->hidden.androidio.fileNameRef = mEnv->NewGlobalRef(fileNameJString);
+
+    return Android_JNI_FileOpen(ctx);
+}
+
+extern "C" size_t Android_JNI_FileRead(SDL_RWops* ctx, void* buffer,
+        size_t size, size_t maxnum)
+{
+    int bytesRemaining = size * maxnum;
+    int bytesRead = 0;
+
+    jobject readableByteChannel = (jobject)ctx->hidden.androidio.readableByteChannel;
+    jmethodID readMethod = (jmethodID)ctx->hidden.androidio.readMethod;
+    jobject byteBuffer = mEnv->NewDirectByteBuffer(buffer, bytesRemaining);
+
+    mEnv->ExceptionClear();
+    while (bytesRemaining > 0) {
+        // result = readableByteChannel.read(...);
+        int result = mEnv->CallIntMethod(readableByteChannel, readMethod, byteBuffer);
+
+        if (mEnv->ExceptionOccurred()) {
+            mEnv->ExceptionDescribe();
+            mEnv->ExceptionClear();
+            return 0;
+        }
+
+        if (result < 0) {
+            break;
+        }
+
+        bytesRemaining -= result;
+        bytesRead += result;
+        ctx->hidden.androidio.position += result;
+    }
+
+    return bytesRead / size;
+}
+
+extern "C" size_t Android_JNI_FileWrite(SDL_RWops* ctx, const void* buffer,
+        size_t size, size_t num)
+{
+    SDL_SetError("Cannot write to Android package filesystem");
+    return 0;
+}
+
+static int Android_JNI_FileClose(SDL_RWops* ctx, bool release)
+{
+    int result = 0;
+
+    if (ctx) {
+        if (release) {
+            mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.fileNameRef);
+        }
+
+        jobject inputStream = (jobject)ctx->hidden.androidio.inputStream;
+
+        // inputStream.close();
+        mEnv->ExceptionClear();
+        jmethodID mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
+                "close", "()V");
+        mEnv->CallVoidMethod(inputStream, mid);
+        mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.inputStreamRef);
+        mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.readableByteChannelRef);
+        if (mEnv->ExceptionOccurred()) {
+            result = -1;
+            mEnv->ExceptionDescribe();
+            mEnv->ExceptionClear();
+        }
+
+        if (release) {
+            SDL_FreeRW(ctx);
+        }
+    }
+
+    return result;
+}
+
+
+extern "C" long Android_JNI_FileSeek(SDL_RWops* ctx, long offset, int whence)
+{
+    long newPosition;
+
+    switch (whence) {
+        case RW_SEEK_SET:
+            newPosition = offset;
+            break;
+        case RW_SEEK_CUR:
+            newPosition = ctx->hidden.androidio.position + offset;
+            break;
+        case RW_SEEK_END:
+            newPosition = ctx->hidden.androidio.size + offset;
+            break;
+        default:
+            SDL_SetError("Unknown value for 'whence'");
+            return -1;
+    }
+    if (newPosition < 0) {
+        newPosition = 0;
+    }
+    if (newPosition > ctx->hidden.androidio.size) {
+        newPosition = ctx->hidden.androidio.size;
+    }
+
+    long movement = newPosition - ctx->hidden.androidio.position;
+    jobject inputStream = (jobject)ctx->hidden.androidio.inputStream;
+    jmethodID skipMethod = (jmethodID)ctx->hidden.androidio.skipMethod;
+
+    if (movement > 0) {
+        // The easy case where we're seeking forwards
+        mEnv->ExceptionClear();
+        while (movement > 0) {
+            // inputStream.skip(...);
+            movement -= mEnv->CallLongMethod(inputStream, skipMethod, movement);
+            if (mEnv->ExceptionOccurred()) {
+                mEnv->ExceptionDescribe();
+                mEnv->ExceptionClear();
+                SDL_SetError("Exception while seeking");
+                return -1;
+            }
+        }
+    } else if (movement < 0) {
+        // We can't seek backwards so we have to reopen the file and seek
+        // forwards which obviously isn't very efficient
+        Android_JNI_FileClose(ctx, false);
+        Android_JNI_FileOpen(ctx);
+        Android_JNI_FileSeek(ctx, newPosition, RW_SEEK_SET);
+    }
+
+    ctx->hidden.androidio.position = newPosition;
+
+    return ctx->hidden.androidio.position;
+}
+
+extern "C" int Android_JNI_FileClose(SDL_RWops* ctx)
+{
+    return Android_JNI_FileClose(ctx, true);
+}
+
 /* vi: set ts=4 sw=4 expandtab: */
--- a/src/core/android/SDL_android.h	Wed Jul 27 18:07:40 2011 -0400
+++ b/src/core/android/SDL_android.h	Fri Jul 29 16:51:25 2011 -0400
@@ -39,6 +39,14 @@
 extern void Android_JNI_WriteAudioBuffer();
 extern void Android_JNI_CloseAudioDevice();
 
+#include "SDL_rwops.h"
+
+int Android_JNI_FileOpen(SDL_RWops* ctx, const char* fileName, const char* mode);
+long Android_JNI_FileSeek(SDL_RWops* ctx, long offset, int whence);
+size_t Android_JNI_FileRead(SDL_RWops* ctx, void* buffer, size_t size, size_t maxnum);
+size_t Android_JNI_FileWrite(SDL_RWops* ctx, const void* buffer, size_t size, size_t num);
+int Android_JNI_FileClose(SDL_RWops* ctx);
+
 /* Ends C function definitions when using C++ */
 #ifdef __cplusplus
 /* *INDENT-OFF* */
--- a/src/file/SDL_rwops.c	Wed Jul 27 18:07:40 2011 -0400
+++ b/src/file/SDL_rwops.c	Fri Jul 29 16:51:25 2011 -0400
@@ -31,6 +31,10 @@
 #include "cocoa/SDL_rwopsbundlesupport.h"
 #endif /* __APPLE__ */
 
+#ifdef ANDROID
+#include "../core/android/SDL_android.h"
+#endif
+
 #ifdef __NDS__
 /* include libfat headers for fatInitDefault(). */
 #include <fat.h>
@@ -441,7 +445,20 @@
         SDL_SetError("SDL_RWFromFile(): No file or no mode specified");
         return NULL;
     }
-#if defined(__WIN32__)
+#if defined(ANDROID)
+    rwops = SDL_AllocRW();
+    if (!rwops)
+        return NULL;            /* SDL_SetError already setup by SDL_AllocRW() */
+    if (Android_JNI_FileOpen(rwops, file, mode) < 0) {
+        SDL_FreeRW(rwops);
+        return NULL;
+    }
+    rwops->seek = Android_JNI_FileSeek;
+    rwops->read = Android_JNI_FileRead;
+    rwops->write = Android_JNI_FileWrite;
+    rwops->close = Android_JNI_FileClose;
+
+#elif defined(__WIN32__)
     rwops = SDL_AllocRW();
     if (!rwops)
         return NULL;            /* SDL_SetError already setup by SDL_AllocRW() */