Android: Access APK files using AssetFileDescriptor
authorGabriel Jacobo <gabomdq@gmail.com>
Tue, 08 Jan 2013 09:30:53 -0300
changeset 6806 9e57ff36fd7a
parent 6805 ef2ef554b662
child 6807 e3610bc90cf3
Android: Access APK files using AssetFileDescriptor
include/SDL_rwops.h
src/core/android/SDL_android.cpp
--- a/include/SDL_rwops.h	Tue Jan 08 08:28:39 2013 -0300
+++ b/include/SDL_rwops.h	Tue Jan 08 09:30:53 2013 -0300
@@ -94,8 +94,11 @@
             void *inputStreamRef;
             void *readableByteChannelRef;
             void *readMethod;
+            void *assetFileDescriptorRef;
             long position;
-            int size;
+            long size;
+            long offset;
+            int fd;
         } androidio;
 #elif defined(__WIN32__)
         struct
--- a/src/core/android/SDL_android.cpp	Tue Jan 08 08:28:39 2013 -0300
+++ b/src/core/android/SDL_android.cpp	Tue Jan 08 09:30:53 2013 -0300
@@ -37,6 +37,8 @@
 
 #include <android/log.h>
 #include <pthread.h>
+#include <sys/types.h>
+#include <unistd.h>
 #define LOG_TAG "SDL_android"
 //#define LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
 //#define LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
@@ -559,6 +561,9 @@
     jclass channels;
     jobject readableByteChannel;
     jstring fileNameJString;
+    jobject fd;
+    jclass fdCls;
+    jfieldID descriptor;
 
     JNIEnv *mEnv = Android_JNI_GetEnv();
     if (!refs.init(mEnv)) {
@@ -566,62 +571,98 @@
     }
 
     fileNameJString = (jstring)ctx->hidden.androidio.fileNameRef;
+    ctx->hidden.androidio.position = 0;
 
     // context = SDLActivity.getContext();
     mid = mEnv->GetStaticMethodID(mActivityClass,
             "getContext","()Landroid/content/Context;");
     context = mEnv->CallStaticObjectMethod(mActivityClass, mid);
+    
 
     // assetManager = context.getAssets();
     mid = mEnv->GetMethodID(mEnv->GetObjectClass(context),
             "getAssets", "()Landroid/content/res/AssetManager;");
     assetManager = mEnv->CallObjectMethod(context, mid);
 
-    // inputStream = assetManager.open(<filename>);
-    mid = mEnv->GetMethodID(mEnv->GetObjectClass(assetManager),
-            "open", "(Ljava/lang/String;)Ljava/io/InputStream;");
+    /* First let's try opening the file to obtain an AssetFileDescriptor.
+    * This method reads the files directly from the APKs using standard *nix calls
+    */
+    mid = mEnv->GetMethodID(mEnv->GetObjectClass(assetManager), "openFd", "(Ljava/lang/String;)Landroid/content/res/AssetFileDescriptor;");
     inputStream = mEnv->CallObjectMethod(assetManager, mid, fileNameJString);
     if (Android_JNI_ExceptionOccurred()) {
-        goto failure;
+        goto fallback;
+    }
+
+    ctx->hidden.androidio.assetFileDescriptorRef = mEnv->NewGlobalRef(inputStream);
+    mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream), "getStartOffset", "()J");
+    ctx->hidden.androidio.offset = mEnv->CallLongMethod(inputStream, mid);
+    if (Android_JNI_ExceptionOccurred()) {
+        goto fallback;
+    }
+
+    mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream), "getDeclaredLength", "()J");
+    ctx->hidden.androidio.size = mEnv->CallLongMethod(inputStream, mid);
+    
+    if (Android_JNI_ExceptionOccurred()) {
+        goto fallback;
     }
 
-    ctx->hidden.androidio.inputStreamRef = mEnv->NewGlobalRef(inputStream);
+    mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream), "getFileDescriptor", "()Ljava/io/FileDescriptor;");
+    fd = mEnv->CallObjectMethod(inputStream, mid);
+    fdCls = mEnv->GetObjectClass(fd);
+    descriptor = mEnv->GetFieldID(fdCls, "descriptor", "I");
+    ctx->hidden.androidio.fd = mEnv->GetIntField(fd, descriptor);
 
-    // 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
+    if (false) {
+fallback:
+        __android_log_print(ANDROID_LOG_DEBUG, "SDL", "Falling back to legacy InputStream method for opening file");
+        /* Try the old method using InputStream */
+        ctx->hidden.androidio.assetFileDescriptorRef = NULL;
 
-    // size = inputStream.available();
-    mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
-            "available", "()I");
-    ctx->hidden.androidio.size = mEnv->CallIntMethod(inputStream, mid);
-    if (Android_JNI_ExceptionOccurred()) {
-        goto failure;
-    }
+        // inputStream = assetManager.open(<filename>);
+        mid = mEnv->GetMethodID(mEnv->GetObjectClass(assetManager),
+                "open", "(Ljava/lang/String;I)Ljava/io/InputStream;");
+        inputStream = mEnv->CallObjectMethod(assetManager, mid, fileNameJString, 1 /*ACCESS_RANDOM*/);
+        if (Android_JNI_ExceptionOccurred()) {
+            goto failure;
+        }
+
+        ctx->hidden.androidio.inputStreamRef = mEnv->NewGlobalRef(inputStream);
+
+        // 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
 
-    // readableByteChannel = Channels.newChannel(inputStream);
-    channels = mEnv->FindClass("java/nio/channels/Channels");
-    mid = mEnv->GetStaticMethodID(channels,
-            "newChannel",
-            "(Ljava/io/InputStream;)Ljava/nio/channels/ReadableByteChannel;");
-    readableByteChannel = mEnv->CallStaticObjectMethod(
-            channels, mid, inputStream);
-    if (Android_JNI_ExceptionOccurred()) {
-        goto failure;
+        // size = inputStream.available();
+        mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
+                "available", "()I");
+        ctx->hidden.androidio.size = (long)mEnv->CallIntMethod(inputStream, mid);
+        if (Android_JNI_ExceptionOccurred()) {
+            goto failure;
+        }
+
+        // readableByteChannel = Channels.newChannel(inputStream);
+        channels = mEnv->FindClass("java/nio/channels/Channels");
+        mid = mEnv->GetStaticMethodID(channels,
+                "newChannel",
+                "(Ljava/io/InputStream;)Ljava/nio/channels/ReadableByteChannel;");
+        readableByteChannel = mEnv->CallStaticObjectMethod(
+                channels, mid, inputStream);
+        if (Android_JNI_ExceptionOccurred()) {
+            goto failure;
+        }
+
+        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.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;
-
     if (false) {
 failure:
         result = -1;
@@ -636,6 +677,10 @@
             mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.readableByteChannelRef);
         }
 
+        if(ctx->hidden.androidio.assetFileDescriptorRef != NULL) {
+            mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.assetFileDescriptorRef);
+        }
+
     }
 
     return result;
@@ -660,6 +705,7 @@
     ctx->hidden.androidio.inputStreamRef = NULL;
     ctx->hidden.androidio.readableByteChannelRef = NULL;
     ctx->hidden.androidio.readMethod = NULL;
+    ctx->hidden.androidio.assetFileDescriptorRef = NULL;
 
     return Android_JNI_FileOpen(ctx);
 }
@@ -668,40 +714,53 @@
         size_t size, size_t maxnum)
 {
     LocalReferenceHolder refs(__FUNCTION__);
-    jlong bytesRemaining = (jlong) (size * maxnum);
-    jlong bytesMax = (jlong) (ctx->hidden.androidio.size -  ctx->hidden.androidio.position);
-    int bytesRead = 0;
-
-    /* Don't read more bytes than those that remain in the file, otherwise we get an exception */
-    if (bytesRemaining >  bytesMax) bytesRemaining = bytesMax;
 
-    JNIEnv *mEnv = Android_JNI_GetEnv();
-    if (!refs.init(mEnv)) {
-        return -1;
-    }
+    if (ctx->hidden.androidio.assetFileDescriptorRef) {
+        size_t bytesMax = size * maxnum;
+        if (ctx->hidden.androidio.size != -1 /*UNKNOWN_LENGTH*/ && ctx->hidden.androidio.position + bytesMax > ctx->hidden.androidio.size) {
+            bytesMax = ctx->hidden.androidio.size - ctx->hidden.androidio.position;
+        }
+        size_t result = read(ctx->hidden.androidio.fd, buffer, bytesMax );
+        if (result > 0) {
+            ctx->hidden.androidio.position += result;
+            return result / size;
+        }
+        return 0;
+    } else {
+        jlong bytesRemaining = (jlong) (size * maxnum);
+        jlong bytesMax = (jlong) (ctx->hidden.androidio.size -  ctx->hidden.androidio.position);
+        int bytesRead = 0;
 
-    jobject readableByteChannel = (jobject)ctx->hidden.androidio.readableByteChannelRef;
-    jmethodID readMethod = (jmethodID)ctx->hidden.androidio.readMethod;
-    jobject byteBuffer = mEnv->NewDirectByteBuffer(buffer, bytesRemaining);
+        /* Don't read more bytes than those that remain in the file, otherwise we get an exception */
+        if (bytesRemaining >  bytesMax) bytesRemaining = bytesMax;
 
-    while (bytesRemaining > 0) {
-        // result = readableByteChannel.read(...);
-        int result = mEnv->CallIntMethod(readableByteChannel, readMethod, byteBuffer);
-
-        if (Android_JNI_ExceptionOccurred()) {
-            return 0;
+        JNIEnv *mEnv = Android_JNI_GetEnv();
+        if (!refs.init(mEnv)) {
+            return -1;
         }
 
-        if (result < 0) {
-            break;
-        }
+        jobject readableByteChannel = (jobject)ctx->hidden.androidio.readableByteChannelRef;
+        jmethodID readMethod = (jmethodID)ctx->hidden.androidio.readMethod;
+        jobject byteBuffer = mEnv->NewDirectByteBuffer(buffer, bytesRemaining);
+
+        while (bytesRemaining > 0) {
+            // result = readableByteChannel.read(...);
+            int result = mEnv->CallIntMethod(readableByteChannel, readMethod, byteBuffer);
 
-        bytesRemaining -= result;
-        bytesRead += result;
-        ctx->hidden.androidio.position += result;
-    }
+            if (Android_JNI_ExceptionOccurred()) {
+                return 0;
+            }
 
-    return bytesRead / size;
+            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,
@@ -727,16 +786,28 @@
             mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.fileNameRef);
         }
 
-        jobject inputStream = (jobject)ctx->hidden.androidio.inputStreamRef;
+        if (ctx->hidden.androidio.assetFileDescriptorRef) {
+            jobject inputStream = (jobject)ctx->hidden.androidio.assetFileDescriptorRef;
+            jmethodID mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
+                    "close", "()V");
+            mEnv->CallVoidMethod(inputStream, mid);
+            mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.assetFileDescriptorRef);
+            if (Android_JNI_ExceptionOccurred()) {
+                result = -1;
+            }
+        }
+        else {
+            jobject inputStream = (jobject)ctx->hidden.androidio.inputStreamRef;
 
-        // inputStream.close();
-        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 (Android_JNI_ExceptionOccurred()) {
-            result = -1;
+            // inputStream.close();
+            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 (Android_JNI_ExceptionOccurred()) {
+                result = -1;
+            }
         }
 
         if (release) {
@@ -755,60 +826,86 @@
 
 extern "C" Sint64 Android_JNI_FileSeek(SDL_RWops* ctx, Sint64 offset, int whence)
 {
-    Sint64 newPosition;
+    if (ctx->hidden.androidio.assetFileDescriptorRef) {
+        switch (whence) {
+            case RW_SEEK_SET:
+                if (ctx->hidden.androidio.size != -1 /*UNKNOWN_LENGTH*/ && offset > ctx->hidden.androidio.size) offset = ctx->hidden.androidio.size;
+                offset += ctx->hidden.androidio.offset;
+                break;
+            case RW_SEEK_CUR:
+                offset += ctx->hidden.androidio.position;
+                if (ctx->hidden.androidio.size != -1 /*UNKNOWN_LENGTH*/ && offset > ctx->hidden.androidio.size) offset = ctx->hidden.androidio.size;
+                offset += ctx->hidden.androidio.offset;
+                break;
+            case RW_SEEK_END:
+                offset = ctx->hidden.androidio.offset + ctx->hidden.androidio.size + offset;
+                break;
+            default:
+                SDL_SetError("Unknown value for 'whence'");
+                return -1;
+        }
+        whence = SEEK_SET;
 
-    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;
-    }
+        off_t ret = lseek(ctx->hidden.androidio.fd, (off_t)offset, SEEK_SET);
+        if (ret == -1) return -1;
+        ctx->hidden.androidio.position = ret - ctx->hidden.androidio.offset;
+    } else {
+        Sint64 newPosition;
 
-    /* Validate the new position */
-    if (newPosition < 0) {
-        SDL_Error(SDL_EFSEEK);
-        return -1;
-    }
-    if (newPosition > ctx->hidden.androidio.size) {
-        newPosition = ctx->hidden.androidio.size;
-    }
+        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;
+        }
 
-    Sint64 movement = newPosition - ctx->hidden.androidio.position;
-    if (movement > 0) {
-        unsigned char buffer[4096];
+        /* Validate the new position */
+        if (newPosition < 0) {
+            SDL_Error(SDL_EFSEEK);
+            return -1;
+        }
+        if (newPosition > ctx->hidden.androidio.size) {
+            newPosition = ctx->hidden.androidio.size;
+        }
+
+        Sint64 movement = newPosition - ctx->hidden.androidio.position;
+        if (movement > 0) {
+            unsigned char buffer[4096];
 
-        // The easy case where we're seeking forwards
-        while (movement > 0) {
-            Sint64 amount = sizeof (buffer);
-            if (amount > movement) {
-                amount = movement;
-            }
-            size_t result = Android_JNI_FileRead(ctx, buffer, 1, amount);
-            if (result <= 0) {
-                // Failed to read/skip the required amount, so fail
-                return -1;
+            // The easy case where we're seeking forwards
+            while (movement > 0) {
+                Sint64 amount = sizeof (buffer);
+                if (amount > movement) {
+                    amount = movement;
+                }
+                size_t result = Android_JNI_FileRead(ctx, buffer, 1, amount);
+                if (result <= 0) {
+                    // Failed to read/skip the required amount, so fail
+                    return -1;
+                }
+
+                movement -= result;
             }
 
-            movement -= result;
+        } 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);
         }
-
-    } 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);
     }
 
     return ctx->hidden.androidio.position;
+    
 }
 
 extern "C" int Android_JNI_FileClose(SDL_RWops* ctx)