From a4d3f3ed5bd6bf435e031ab48421cac08828f9d1 Mon Sep 17 00:00:00 2001 From: Gabriel Jacobo Date: Tue, 8 Jan 2013 09:30:53 -0300 Subject: [PATCH] Android: Access APK files using AssetFileDescriptor --- include/SDL_rwops.h | 5 +- src/core/android/SDL_android.cpp | 321 ++++++++++++++++++++----------- 2 files changed, 213 insertions(+), 113 deletions(-) diff --git a/include/SDL_rwops.h b/include/SDL_rwops.h index dbd2a0ce0..1901a71bf 100644 --- a/include/SDL_rwops.h +++ b/include/SDL_rwops.h @@ -94,8 +94,11 @@ typedef struct SDL_RWops void *inputStreamRef; void *readableByteChannelRef; void *readMethod; + void *assetFileDescriptorRef; long position; - int size; + long size; + long offset; + int fd; } androidio; #elif defined(__WIN32__) struct diff --git a/src/core/android/SDL_android.cpp b/src/core/android/SDL_android.cpp index c279f64c0..5bcee8702 100644 --- a/src/core/android/SDL_android.cpp +++ b/src/core/android/SDL_android.cpp @@ -37,6 +37,8 @@ extern "C" { #include #include +#include +#include #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 @@ static int Android_JNI_FileOpen(SDL_RWops* ctx) jclass channels; jobject readableByteChannel; jstring fileNameJString; + jobject fd; + jclass fdCls; + jfieldID descriptor; JNIEnv *mEnv = Android_JNI_GetEnv(); if (!refs.init(mEnv)) { @@ -566,61 +571,97 @@ static int Android_JNI_FileOpen(SDL_RWops* ctx) } 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(); - 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.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 /samples//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(); - mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream), - "available", "()I"); - ctx->hidden.androidio.size = mEnv->CallIntMethod(inputStream, mid); + 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 failure; + goto fallback; } - // 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); + mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream), "getDeclaredLength", "()J"); + ctx->hidden.androidio.size = mEnv->CallLongMethod(inputStream, mid); + if (Android_JNI_ExceptionOccurred()) { - goto failure; + goto fallback; } - ctx->hidden.androidio.readableByteChannelRef = - mEnv->NewGlobalRef(readableByteChannel); + 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); - // Store .read id for reading purposes - mid = mEnv->GetMethodID(mEnv->GetObjectClass(readableByteChannel), - "read", "(Ljava/nio/ByteBuffer;)I"); - ctx->hidden.androidio.readMethod = mid; + 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; + + // inputStream = assetManager.open(); + 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.position = 0; + 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 /samples//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(); + 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; + } if (false) { failure: @@ -636,6 +677,10 @@ static int Android_JNI_FileOpen(SDL_RWops* ctx) 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 @@ extern "C" int Android_JNI_FileOpen(SDL_RWops* ctx, 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 @@ extern "C" size_t Android_JNI_FileRead(SDL_RWops* ctx, void* buffer, 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; + 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; - JNIEnv *mEnv = Android_JNI_GetEnv(); - if (!refs.init(mEnv)) { - return -1; - } + /* Don't read more bytes than those that remain in the file, otherwise we get an exception */ + if (bytesRemaining > bytesMax) bytesRemaining = bytesMax; - jobject readableByteChannel = (jobject)ctx->hidden.androidio.readableByteChannelRef; - jmethodID readMethod = (jmethodID)ctx->hidden.androidio.readMethod; - jobject byteBuffer = mEnv->NewDirectByteBuffer(buffer, bytesRemaining); + JNIEnv *mEnv = Android_JNI_GetEnv(); + if (!refs.init(mEnv)) { + return -1; + } - while (bytesRemaining > 0) { - // result = readableByteChannel.read(...); - int result = mEnv->CallIntMethod(readableByteChannel, readMethod, byteBuffer); + jobject readableByteChannel = (jobject)ctx->hidden.androidio.readableByteChannelRef; + jmethodID readMethod = (jmethodID)ctx->hidden.androidio.readMethod; + jobject byteBuffer = mEnv->NewDirectByteBuffer(buffer, bytesRemaining); - if (Android_JNI_ExceptionOccurred()) { - return 0; - } + while (bytesRemaining > 0) { + // result = readableByteChannel.read(...); + int result = mEnv->CallIntMethod(readableByteChannel, readMethod, byteBuffer); - if (result < 0) { - break; - } + if (Android_JNI_ExceptionOccurred()) { + return 0; + } - bytesRemaining -= result; - bytesRead += result; - ctx->hidden.androidio.position += result; - } + if (result < 0) { + break; + } - return bytesRead / size; + 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 @@ static int Android_JNI_FileClose(SDL_RWops* ctx, bool release) 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_FileSize(SDL_RWops* ctx) extern "C" Sint64 Android_JNI_FileSeek(SDL_RWops* ctx, Sint64 offset, int whence) { - Sint64 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 (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; - /* Validate the new position */ - if (newPosition < 0) { - SDL_Error(SDL_EFSEEK); - return -1; - } - if (newPosition > ctx->hidden.androidio.size) { - newPosition = ctx->hidden.androidio.size; - } + 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; + + 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; + } - // 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; + 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; + } + + 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)