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 55821281a3f1f0a6
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
     1.1 --- a/android-project/src/org/libsdl/app/SDLActivity.java	Wed Jul 27 18:07:40 2011 -0400
     1.2 +++ b/android-project/src/org/libsdl/app/SDLActivity.java	Fri Jul 29 16:51:25 2011 -0400
     1.3 @@ -114,6 +114,10 @@
     1.4          mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title);
     1.5      }
     1.6  
     1.7 +	public static Context getContext() {
     1.8 +		return mSingleton;
     1.9 +	}
    1.10 +
    1.11      // Audio
    1.12      private static Object buf;
    1.13      
     2.1 --- a/include/SDL_rwops.h	Wed Jul 27 18:07:40 2011 -0400
     2.2 +++ b/include/SDL_rwops.h	Fri Jul 29 16:51:25 2011 -0400
     2.3 @@ -82,7 +82,21 @@
     2.4      Uint32 type;
     2.5      union
     2.6      {
     2.7 -#ifdef __WIN32__
     2.8 +#if defined(ANDROID)
     2.9 +        struct
    2.10 +        {
    2.11 +            void *fileName;
    2.12 +            void *fileNameRef;
    2.13 +            void *inputStream;
    2.14 +            void *inputStreamRef;
    2.15 +            void *skipMethod;
    2.16 +            void *readableByteChannel;
    2.17 +            void *readableByteChannelRef;
    2.18 +            void *readMethod;
    2.19 +            long position;
    2.20 +            int size;
    2.21 +        } androidio;
    2.22 +#elif defined(__WIN32__)
    2.23          struct
    2.24          {
    2.25              SDL_bool append;
    2.26 @@ -95,6 +109,7 @@
    2.27              } buffer;
    2.28          } windowsio;
    2.29  #endif
    2.30 +
    2.31  #ifdef HAVE_STDIO_H
    2.32          struct
    2.33          {
     3.1 --- a/src/core/android/SDL_android.cpp	Wed Jul 27 18:07:40 2011 -0400
     3.2 +++ b/src/core/android/SDL_android.cpp	Fri Jul 29 16:51:25 2011 -0400
     3.3 @@ -259,4 +259,234 @@
     3.4      }
     3.5  }
     3.6  
     3.7 +static int Android_JNI_FileOpen(SDL_RWops* ctx)
     3.8 +{
     3.9 +    jstring fileNameJString = (jstring)ctx->hidden.androidio.fileName;
    3.10 +
    3.11 +    // context = SDLActivity.getContext();
    3.12 +    jmethodID mid = mEnv->GetStaticMethodID(mActivityClass,
    3.13 +            "getContext","()Landroid/content/Context;");
    3.14 +    jobject context = mEnv->CallStaticObjectMethod(mActivityClass, mid);
    3.15 +
    3.16 +    // assetManager = context.getAssets();
    3.17 +    mid = mEnv->GetMethodID(mEnv->GetObjectClass(context),
    3.18 +            "getAssets","()Landroid/content/res/AssetManager;");
    3.19 +    jobject assetManager = mEnv->CallObjectMethod(context, mid);
    3.20 +
    3.21 +    // inputStream = assetManager.open(<filename>);
    3.22 +    mEnv->ExceptionClear();
    3.23 +    mid = mEnv->GetMethodID(mEnv->GetObjectClass(assetManager),
    3.24 +            "open", "(Ljava/lang/String;)Ljava/io/InputStream;");
    3.25 +    jobject inputStream = mEnv->CallObjectMethod(assetManager, mid, fileNameJString);
    3.26 +    if (mEnv->ExceptionOccurred()) {
    3.27 +        mEnv->ExceptionDescribe();
    3.28 +        mEnv->ExceptionClear();
    3.29 +        mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.fileNameRef);
    3.30 +        return -1;
    3.31 +    } else {
    3.32 +        ctx->hidden.androidio.inputStream = inputStream;
    3.33 +        ctx->hidden.androidio.inputStreamRef = mEnv->NewGlobalRef(inputStream);
    3.34 +    }
    3.35 +
    3.36 +    // Store .skip id for seeking purposes
    3.37 +    mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
    3.38 +            "skip", "(J)J");
    3.39 +    ctx->hidden.androidio.skipMethod = mid;
    3.40 +
    3.41 +    // Despite all the visible documentation on [Asset]InputStream claiming
    3.42 +    // that the .available() method is not guaranteed to return the entire file
    3.43 +    // size, comments in <sdk>/samples/<ver>/ApiDemos/src/com/example/ ...
    3.44 +    // android/apis/content/ReadAsset.java imply that Android's
    3.45 +    // AssetInputStream.available() /will/ always return the total file size
    3.46 +
    3.47 +    // size = inputStream.available();
    3.48 +    mEnv->ExceptionClear();
    3.49 +    mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
    3.50 +            "available", "()I");
    3.51 +    ctx->hidden.androidio.size = mEnv->CallIntMethod(inputStream, mid);
    3.52 +    if (mEnv->ExceptionOccurred()) {
    3.53 +        mEnv->ExceptionDescribe();
    3.54 +        mEnv->ExceptionClear();
    3.55 +        mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.fileNameRef);
    3.56 +        mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.inputStreamRef);
    3.57 +        return -1;
    3.58 +    }
    3.59 +
    3.60 +    // readableByteChannel = Channels.newChannel(inputStream);
    3.61 +    mEnv->ExceptionClear();
    3.62 +    jclass channels = mEnv->FindClass("java/nio/channels/Channels");
    3.63 +    mid = mEnv->GetStaticMethodID(channels,
    3.64 +            "newChannel",
    3.65 +            "(Ljava/io/InputStream;)Ljava/nio/channels/ReadableByteChannel;");
    3.66 +    jobject readableByteChannel = mEnv->CallStaticObjectMethod(
    3.67 +            channels, mid, inputStream);
    3.68 +    if (mEnv->ExceptionOccurred()) {
    3.69 +        mEnv->ExceptionDescribe();
    3.70 +        mEnv->ExceptionClear();
    3.71 +        mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.fileNameRef);
    3.72 +        mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.inputStreamRef);
    3.73 +        return -1;
    3.74 +    } else {
    3.75 +        ctx->hidden.androidio.readableByteChannel = readableByteChannel;
    3.76 +        ctx->hidden.androidio.readableByteChannelRef =
    3.77 +            mEnv->NewGlobalRef(readableByteChannel);
    3.78 +    }
    3.79 +
    3.80 +    // Store .read id for reading purposes
    3.81 +    mid = mEnv->GetMethodID(mEnv->GetObjectClass(readableByteChannel),
    3.82 +            "read", "(Ljava/nio/ByteBuffer;)I");
    3.83 +    ctx->hidden.androidio.readMethod = mid;
    3.84 +
    3.85 +    ctx->hidden.androidio.position = 0;
    3.86 +
    3.87 +    return 0;
    3.88 +}
    3.89 +
    3.90 +extern "C" int Android_JNI_FileOpen(SDL_RWops* ctx,
    3.91 +        const char* fileName, const char*)
    3.92 +{
    3.93 +    if (!ctx) {
    3.94 +        return -1;
    3.95 +    }
    3.96 +
    3.97 +    jstring fileNameJString = mEnv->NewStringUTF(fileName);
    3.98 +    ctx->hidden.androidio.fileName = fileNameJString;
    3.99 +    ctx->hidden.androidio.fileNameRef = mEnv->NewGlobalRef(fileNameJString);
   3.100 +
   3.101 +    return Android_JNI_FileOpen(ctx);
   3.102 +}
   3.103 +
   3.104 +extern "C" size_t Android_JNI_FileRead(SDL_RWops* ctx, void* buffer,
   3.105 +        size_t size, size_t maxnum)
   3.106 +{
   3.107 +    int bytesRemaining = size * maxnum;
   3.108 +    int bytesRead = 0;
   3.109 +
   3.110 +    jobject readableByteChannel = (jobject)ctx->hidden.androidio.readableByteChannel;
   3.111 +    jmethodID readMethod = (jmethodID)ctx->hidden.androidio.readMethod;
   3.112 +    jobject byteBuffer = mEnv->NewDirectByteBuffer(buffer, bytesRemaining);
   3.113 +
   3.114 +    mEnv->ExceptionClear();
   3.115 +    while (bytesRemaining > 0) {
   3.116 +        // result = readableByteChannel.read(...);
   3.117 +        int result = mEnv->CallIntMethod(readableByteChannel, readMethod, byteBuffer);
   3.118 +
   3.119 +        if (mEnv->ExceptionOccurred()) {
   3.120 +            mEnv->ExceptionDescribe();
   3.121 +            mEnv->ExceptionClear();
   3.122 +            return 0;
   3.123 +        }
   3.124 +
   3.125 +        if (result < 0) {
   3.126 +            break;
   3.127 +        }
   3.128 +
   3.129 +        bytesRemaining -= result;
   3.130 +        bytesRead += result;
   3.131 +        ctx->hidden.androidio.position += result;
   3.132 +    }
   3.133 +
   3.134 +    return bytesRead / size;
   3.135 +}
   3.136 +
   3.137 +extern "C" size_t Android_JNI_FileWrite(SDL_RWops* ctx, const void* buffer,
   3.138 +        size_t size, size_t num)
   3.139 +{
   3.140 +    SDL_SetError("Cannot write to Android package filesystem");
   3.141 +    return 0;
   3.142 +}
   3.143 +
   3.144 +static int Android_JNI_FileClose(SDL_RWops* ctx, bool release)
   3.145 +{
   3.146 +    int result = 0;
   3.147 +
   3.148 +    if (ctx) {
   3.149 +        if (release) {
   3.150 +            mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.fileNameRef);
   3.151 +        }
   3.152 +
   3.153 +        jobject inputStream = (jobject)ctx->hidden.androidio.inputStream;
   3.154 +
   3.155 +        // inputStream.close();
   3.156 +        mEnv->ExceptionClear();
   3.157 +        jmethodID mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
   3.158 +                "close", "()V");
   3.159 +        mEnv->CallVoidMethod(inputStream, mid);
   3.160 +        mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.inputStreamRef);
   3.161 +        mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.readableByteChannelRef);
   3.162 +        if (mEnv->ExceptionOccurred()) {
   3.163 +            result = -1;
   3.164 +            mEnv->ExceptionDescribe();
   3.165 +            mEnv->ExceptionClear();
   3.166 +        }
   3.167 +
   3.168 +        if (release) {
   3.169 +            SDL_FreeRW(ctx);
   3.170 +        }
   3.171 +    }
   3.172 +
   3.173 +    return result;
   3.174 +}
   3.175 +
   3.176 +
   3.177 +extern "C" long Android_JNI_FileSeek(SDL_RWops* ctx, long offset, int whence)
   3.178 +{
   3.179 +    long newPosition;
   3.180 +
   3.181 +    switch (whence) {
   3.182 +        case RW_SEEK_SET:
   3.183 +            newPosition = offset;
   3.184 +            break;
   3.185 +        case RW_SEEK_CUR:
   3.186 +            newPosition = ctx->hidden.androidio.position + offset;
   3.187 +            break;
   3.188 +        case RW_SEEK_END:
   3.189 +            newPosition = ctx->hidden.androidio.size + offset;
   3.190 +            break;
   3.191 +        default:
   3.192 +            SDL_SetError("Unknown value for 'whence'");
   3.193 +            return -1;
   3.194 +    }
   3.195 +    if (newPosition < 0) {
   3.196 +        newPosition = 0;
   3.197 +    }
   3.198 +    if (newPosition > ctx->hidden.androidio.size) {
   3.199 +        newPosition = ctx->hidden.androidio.size;
   3.200 +    }
   3.201 +
   3.202 +    long movement = newPosition - ctx->hidden.androidio.position;
   3.203 +    jobject inputStream = (jobject)ctx->hidden.androidio.inputStream;
   3.204 +    jmethodID skipMethod = (jmethodID)ctx->hidden.androidio.skipMethod;
   3.205 +
   3.206 +    if (movement > 0) {
   3.207 +        // The easy case where we're seeking forwards
   3.208 +        mEnv->ExceptionClear();
   3.209 +        while (movement > 0) {
   3.210 +            // inputStream.skip(...);
   3.211 +            movement -= mEnv->CallLongMethod(inputStream, skipMethod, movement);
   3.212 +            if (mEnv->ExceptionOccurred()) {
   3.213 +                mEnv->ExceptionDescribe();
   3.214 +                mEnv->ExceptionClear();
   3.215 +                SDL_SetError("Exception while seeking");
   3.216 +                return -1;
   3.217 +            }
   3.218 +        }
   3.219 +    } else if (movement < 0) {
   3.220 +        // We can't seek backwards so we have to reopen the file and seek
   3.221 +        // forwards which obviously isn't very efficient
   3.222 +        Android_JNI_FileClose(ctx, false);
   3.223 +        Android_JNI_FileOpen(ctx);
   3.224 +        Android_JNI_FileSeek(ctx, newPosition, RW_SEEK_SET);
   3.225 +    }
   3.226 +
   3.227 +    ctx->hidden.androidio.position = newPosition;
   3.228 +
   3.229 +    return ctx->hidden.androidio.position;
   3.230 +}
   3.231 +
   3.232 +extern "C" int Android_JNI_FileClose(SDL_RWops* ctx)
   3.233 +{
   3.234 +    return Android_JNI_FileClose(ctx, true);
   3.235 +}
   3.236 +
   3.237  /* vi: set ts=4 sw=4 expandtab: */
     4.1 --- a/src/core/android/SDL_android.h	Wed Jul 27 18:07:40 2011 -0400
     4.2 +++ b/src/core/android/SDL_android.h	Fri Jul 29 16:51:25 2011 -0400
     4.3 @@ -39,6 +39,14 @@
     4.4  extern void Android_JNI_WriteAudioBuffer();
     4.5  extern void Android_JNI_CloseAudioDevice();
     4.6  
     4.7 +#include "SDL_rwops.h"
     4.8 +
     4.9 +int Android_JNI_FileOpen(SDL_RWops* ctx, const char* fileName, const char* mode);
    4.10 +long Android_JNI_FileSeek(SDL_RWops* ctx, long offset, int whence);
    4.11 +size_t Android_JNI_FileRead(SDL_RWops* ctx, void* buffer, size_t size, size_t maxnum);
    4.12 +size_t Android_JNI_FileWrite(SDL_RWops* ctx, const void* buffer, size_t size, size_t num);
    4.13 +int Android_JNI_FileClose(SDL_RWops* ctx);
    4.14 +
    4.15  /* Ends C function definitions when using C++ */
    4.16  #ifdef __cplusplus
    4.17  /* *INDENT-OFF* */
     5.1 --- a/src/file/SDL_rwops.c	Wed Jul 27 18:07:40 2011 -0400
     5.2 +++ b/src/file/SDL_rwops.c	Fri Jul 29 16:51:25 2011 -0400
     5.3 @@ -31,6 +31,10 @@
     5.4  #include "cocoa/SDL_rwopsbundlesupport.h"
     5.5  #endif /* __APPLE__ */
     5.6  
     5.7 +#ifdef ANDROID
     5.8 +#include "../core/android/SDL_android.h"
     5.9 +#endif
    5.10 +
    5.11  #ifdef __NDS__
    5.12  /* include libfat headers for fatInitDefault(). */
    5.13  #include <fat.h>
    5.14 @@ -441,7 +445,20 @@
    5.15          SDL_SetError("SDL_RWFromFile(): No file or no mode specified");
    5.16          return NULL;
    5.17      }
    5.18 -#if defined(__WIN32__)
    5.19 +#if defined(ANDROID)
    5.20 +    rwops = SDL_AllocRW();
    5.21 +    if (!rwops)
    5.22 +        return NULL;            /* SDL_SetError already setup by SDL_AllocRW() */
    5.23 +    if (Android_JNI_FileOpen(rwops, file, mode) < 0) {
    5.24 +        SDL_FreeRW(rwops);
    5.25 +        return NULL;
    5.26 +    }
    5.27 +    rwops->seek = Android_JNI_FileSeek;
    5.28 +    rwops->read = Android_JNI_FileRead;
    5.29 +    rwops->write = Android_JNI_FileWrite;
    5.30 +    rwops->close = Android_JNI_FileClose;
    5.31 +
    5.32 +#elif defined(__WIN32__)
    5.33      rwops = SDL_AllocRW();
    5.34      if (!rwops)
    5.35          return NULL;            /* SDL_SetError already setup by SDL_AllocRW() */