Android: Access APK files using AssetFileDescriptor
authorGabriel Jacobo <gabomdq@gmail.com>
Tue, 08 Jan 2013 09:30:53 -0300
changeset 68069e57ff36fd7a
parent 6805 ef2ef554b662
child 6807 e3610bc90cf3
Android: Access APK files using AssetFileDescriptor
include/SDL_rwops.h
src/core/android/SDL_android.cpp
     1.1 --- a/include/SDL_rwops.h	Tue Jan 08 08:28:39 2013 -0300
     1.2 +++ b/include/SDL_rwops.h	Tue Jan 08 09:30:53 2013 -0300
     1.3 @@ -94,8 +94,11 @@
     1.4              void *inputStreamRef;
     1.5              void *readableByteChannelRef;
     1.6              void *readMethod;
     1.7 +            void *assetFileDescriptorRef;
     1.8              long position;
     1.9 -            int size;
    1.10 +            long size;
    1.11 +            long offset;
    1.12 +            int fd;
    1.13          } androidio;
    1.14  #elif defined(__WIN32__)
    1.15          struct
     2.1 --- a/src/core/android/SDL_android.cpp	Tue Jan 08 08:28:39 2013 -0300
     2.2 +++ b/src/core/android/SDL_android.cpp	Tue Jan 08 09:30:53 2013 -0300
     2.3 @@ -37,6 +37,8 @@
     2.4  
     2.5  #include <android/log.h>
     2.6  #include <pthread.h>
     2.7 +#include <sys/types.h>
     2.8 +#include <unistd.h>
     2.9  #define LOG_TAG "SDL_android"
    2.10  //#define LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
    2.11  //#define LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
    2.12 @@ -559,6 +561,9 @@
    2.13      jclass channels;
    2.14      jobject readableByteChannel;
    2.15      jstring fileNameJString;
    2.16 +    jobject fd;
    2.17 +    jclass fdCls;
    2.18 +    jfieldID descriptor;
    2.19  
    2.20      JNIEnv *mEnv = Android_JNI_GetEnv();
    2.21      if (!refs.init(mEnv)) {
    2.22 @@ -566,61 +571,97 @@
    2.23      }
    2.24  
    2.25      fileNameJString = (jstring)ctx->hidden.androidio.fileNameRef;
    2.26 +    ctx->hidden.androidio.position = 0;
    2.27  
    2.28      // context = SDLActivity.getContext();
    2.29      mid = mEnv->GetStaticMethodID(mActivityClass,
    2.30              "getContext","()Landroid/content/Context;");
    2.31      context = mEnv->CallStaticObjectMethod(mActivityClass, mid);
    2.32 +    
    2.33  
    2.34      // assetManager = context.getAssets();
    2.35      mid = mEnv->GetMethodID(mEnv->GetObjectClass(context),
    2.36              "getAssets", "()Landroid/content/res/AssetManager;");
    2.37      assetManager = mEnv->CallObjectMethod(context, mid);
    2.38  
    2.39 -    // inputStream = assetManager.open(<filename>);
    2.40 -    mid = mEnv->GetMethodID(mEnv->GetObjectClass(assetManager),
    2.41 -            "open", "(Ljava/lang/String;)Ljava/io/InputStream;");
    2.42 +    /* First let's try opening the file to obtain an AssetFileDescriptor.
    2.43 +    * This method reads the files directly from the APKs using standard *nix calls
    2.44 +    */
    2.45 +    mid = mEnv->GetMethodID(mEnv->GetObjectClass(assetManager), "openFd", "(Ljava/lang/String;)Landroid/content/res/AssetFileDescriptor;");
    2.46      inputStream = mEnv->CallObjectMethod(assetManager, mid, fileNameJString);
    2.47      if (Android_JNI_ExceptionOccurred()) {
    2.48 -        goto failure;
    2.49 +        goto fallback;
    2.50      }
    2.51  
    2.52 -    ctx->hidden.androidio.inputStreamRef = mEnv->NewGlobalRef(inputStream);
    2.53 -
    2.54 -    // Despite all the visible documentation on [Asset]InputStream claiming
    2.55 -    // that the .available() method is not guaranteed to return the entire file
    2.56 -    // size, comments in <sdk>/samples/<ver>/ApiDemos/src/com/example/ ...
    2.57 -    // android/apis/content/ReadAsset.java imply that Android's
    2.58 -    // AssetInputStream.available() /will/ always return the total file size
    2.59 -
    2.60 -    // size = inputStream.available();
    2.61 -    mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
    2.62 -            "available", "()I");
    2.63 -    ctx->hidden.androidio.size = mEnv->CallIntMethod(inputStream, mid);
    2.64 +    ctx->hidden.androidio.assetFileDescriptorRef = mEnv->NewGlobalRef(inputStream);
    2.65 +    mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream), "getStartOffset", "()J");
    2.66 +    ctx->hidden.androidio.offset = mEnv->CallLongMethod(inputStream, mid);
    2.67      if (Android_JNI_ExceptionOccurred()) {
    2.68 -        goto failure;
    2.69 +        goto fallback;
    2.70      }
    2.71  
    2.72 -    // readableByteChannel = Channels.newChannel(inputStream);
    2.73 -    channels = mEnv->FindClass("java/nio/channels/Channels");
    2.74 -    mid = mEnv->GetStaticMethodID(channels,
    2.75 -            "newChannel",
    2.76 -            "(Ljava/io/InputStream;)Ljava/nio/channels/ReadableByteChannel;");
    2.77 -    readableByteChannel = mEnv->CallStaticObjectMethod(
    2.78 -            channels, mid, inputStream);
    2.79 +    mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream), "getDeclaredLength", "()J");
    2.80 +    ctx->hidden.androidio.size = mEnv->CallLongMethod(inputStream, mid);
    2.81 +    
    2.82      if (Android_JNI_ExceptionOccurred()) {
    2.83 -        goto failure;
    2.84 +        goto fallback;
    2.85      }
    2.86  
    2.87 -    ctx->hidden.androidio.readableByteChannelRef =
    2.88 -        mEnv->NewGlobalRef(readableByteChannel);
    2.89 +    mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream), "getFileDescriptor", "()Ljava/io/FileDescriptor;");
    2.90 +    fd = mEnv->CallObjectMethod(inputStream, mid);
    2.91 +    fdCls = mEnv->GetObjectClass(fd);
    2.92 +    descriptor = mEnv->GetFieldID(fdCls, "descriptor", "I");
    2.93 +    ctx->hidden.androidio.fd = mEnv->GetIntField(fd, descriptor);
    2.94  
    2.95 -    // Store .read id for reading purposes
    2.96 -    mid = mEnv->GetMethodID(mEnv->GetObjectClass(readableByteChannel),
    2.97 -            "read", "(Ljava/nio/ByteBuffer;)I");
    2.98 -    ctx->hidden.androidio.readMethod = mid;
    2.99 +    if (false) {
   2.100 +fallback:
   2.101 +        __android_log_print(ANDROID_LOG_DEBUG, "SDL", "Falling back to legacy InputStream method for opening file");
   2.102 +        /* Try the old method using InputStream */
   2.103 +        ctx->hidden.androidio.assetFileDescriptorRef = NULL;
   2.104  
   2.105 -    ctx->hidden.androidio.position = 0;
   2.106 +        // inputStream = assetManager.open(<filename>);
   2.107 +        mid = mEnv->GetMethodID(mEnv->GetObjectClass(assetManager),
   2.108 +                "open", "(Ljava/lang/String;I)Ljava/io/InputStream;");
   2.109 +        inputStream = mEnv->CallObjectMethod(assetManager, mid, fileNameJString, 1 /*ACCESS_RANDOM*/);
   2.110 +        if (Android_JNI_ExceptionOccurred()) {
   2.111 +            goto failure;
   2.112 +        }
   2.113 +
   2.114 +        ctx->hidden.androidio.inputStreamRef = mEnv->NewGlobalRef(inputStream);
   2.115 +
   2.116 +        // Despite all the visible documentation on [Asset]InputStream claiming
   2.117 +        // that the .available() method is not guaranteed to return the entire file
   2.118 +        // size, comments in <sdk>/samples/<ver>/ApiDemos/src/com/example/ ...
   2.119 +        // android/apis/content/ReadAsset.java imply that Android's
   2.120 +        // AssetInputStream.available() /will/ always return the total file size
   2.121 +
   2.122 +        // size = inputStream.available();
   2.123 +        mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
   2.124 +                "available", "()I");
   2.125 +        ctx->hidden.androidio.size = (long)mEnv->CallIntMethod(inputStream, mid);
   2.126 +        if (Android_JNI_ExceptionOccurred()) {
   2.127 +            goto failure;
   2.128 +        }
   2.129 +
   2.130 +        // readableByteChannel = Channels.newChannel(inputStream);
   2.131 +        channels = mEnv->FindClass("java/nio/channels/Channels");
   2.132 +        mid = mEnv->GetStaticMethodID(channels,
   2.133 +                "newChannel",
   2.134 +                "(Ljava/io/InputStream;)Ljava/nio/channels/ReadableByteChannel;");
   2.135 +        readableByteChannel = mEnv->CallStaticObjectMethod(
   2.136 +                channels, mid, inputStream);
   2.137 +        if (Android_JNI_ExceptionOccurred()) {
   2.138 +            goto failure;
   2.139 +        }
   2.140 +
   2.141 +        ctx->hidden.androidio.readableByteChannelRef =
   2.142 +            mEnv->NewGlobalRef(readableByteChannel);
   2.143 +
   2.144 +        // Store .read id for reading purposes
   2.145 +        mid = mEnv->GetMethodID(mEnv->GetObjectClass(readableByteChannel),
   2.146 +                "read", "(Ljava/nio/ByteBuffer;)I");
   2.147 +        ctx->hidden.androidio.readMethod = mid;
   2.148 +    }
   2.149  
   2.150      if (false) {
   2.151  failure:
   2.152 @@ -636,6 +677,10 @@
   2.153              mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.readableByteChannelRef);
   2.154          }
   2.155  
   2.156 +        if(ctx->hidden.androidio.assetFileDescriptorRef != NULL) {
   2.157 +            mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.assetFileDescriptorRef);
   2.158 +        }
   2.159 +
   2.160      }
   2.161  
   2.162      return result;
   2.163 @@ -660,6 +705,7 @@
   2.164      ctx->hidden.androidio.inputStreamRef = NULL;
   2.165      ctx->hidden.androidio.readableByteChannelRef = NULL;
   2.166      ctx->hidden.androidio.readMethod = NULL;
   2.167 +    ctx->hidden.androidio.assetFileDescriptorRef = NULL;
   2.168  
   2.169      return Android_JNI_FileOpen(ctx);
   2.170  }
   2.171 @@ -668,40 +714,53 @@
   2.172          size_t size, size_t maxnum)
   2.173  {
   2.174      LocalReferenceHolder refs(__FUNCTION__);
   2.175 -    jlong bytesRemaining = (jlong) (size * maxnum);
   2.176 -    jlong bytesMax = (jlong) (ctx->hidden.androidio.size -  ctx->hidden.androidio.position);
   2.177 -    int bytesRead = 0;
   2.178  
   2.179 -    /* Don't read more bytes than those that remain in the file, otherwise we get an exception */
   2.180 -    if (bytesRemaining >  bytesMax) bytesRemaining = bytesMax;
   2.181 +    if (ctx->hidden.androidio.assetFileDescriptorRef) {
   2.182 +        size_t bytesMax = size * maxnum;
   2.183 +        if (ctx->hidden.androidio.size != -1 /*UNKNOWN_LENGTH*/ && ctx->hidden.androidio.position + bytesMax > ctx->hidden.androidio.size) {
   2.184 +            bytesMax = ctx->hidden.androidio.size - ctx->hidden.androidio.position;
   2.185 +        }
   2.186 +        size_t result = read(ctx->hidden.androidio.fd, buffer, bytesMax );
   2.187 +        if (result > 0) {
   2.188 +            ctx->hidden.androidio.position += result;
   2.189 +            return result / size;
   2.190 +        }
   2.191 +        return 0;
   2.192 +    } else {
   2.193 +        jlong bytesRemaining = (jlong) (size * maxnum);
   2.194 +        jlong bytesMax = (jlong) (ctx->hidden.androidio.size -  ctx->hidden.androidio.position);
   2.195 +        int bytesRead = 0;
   2.196  
   2.197 -    JNIEnv *mEnv = Android_JNI_GetEnv();
   2.198 -    if (!refs.init(mEnv)) {
   2.199 -        return -1;
   2.200 -    }
   2.201 +        /* Don't read more bytes than those that remain in the file, otherwise we get an exception */
   2.202 +        if (bytesRemaining >  bytesMax) bytesRemaining = bytesMax;
   2.203  
   2.204 -    jobject readableByteChannel = (jobject)ctx->hidden.androidio.readableByteChannelRef;
   2.205 -    jmethodID readMethod = (jmethodID)ctx->hidden.androidio.readMethod;
   2.206 -    jobject byteBuffer = mEnv->NewDirectByteBuffer(buffer, bytesRemaining);
   2.207 -
   2.208 -    while (bytesRemaining > 0) {
   2.209 -        // result = readableByteChannel.read(...);
   2.210 -        int result = mEnv->CallIntMethod(readableByteChannel, readMethod, byteBuffer);
   2.211 -
   2.212 -        if (Android_JNI_ExceptionOccurred()) {
   2.213 -            return 0;
   2.214 +        JNIEnv *mEnv = Android_JNI_GetEnv();
   2.215 +        if (!refs.init(mEnv)) {
   2.216 +            return -1;
   2.217          }
   2.218  
   2.219 -        if (result < 0) {
   2.220 -            break;
   2.221 +        jobject readableByteChannel = (jobject)ctx->hidden.androidio.readableByteChannelRef;
   2.222 +        jmethodID readMethod = (jmethodID)ctx->hidden.androidio.readMethod;
   2.223 +        jobject byteBuffer = mEnv->NewDirectByteBuffer(buffer, bytesRemaining);
   2.224 +
   2.225 +        while (bytesRemaining > 0) {
   2.226 +            // result = readableByteChannel.read(...);
   2.227 +            int result = mEnv->CallIntMethod(readableByteChannel, readMethod, byteBuffer);
   2.228 +
   2.229 +            if (Android_JNI_ExceptionOccurred()) {
   2.230 +                return 0;
   2.231 +            }
   2.232 +
   2.233 +            if (result < 0) {
   2.234 +                break;
   2.235 +            }
   2.236 +
   2.237 +            bytesRemaining -= result;
   2.238 +            bytesRead += result;
   2.239 +            ctx->hidden.androidio.position += result;
   2.240          }
   2.241 -
   2.242 -        bytesRemaining -= result;
   2.243 -        bytesRead += result;
   2.244 -        ctx->hidden.androidio.position += result;
   2.245 -    }
   2.246 -
   2.247 -    return bytesRead / size;
   2.248 +        return bytesRead / size;
   2.249 +    }    
   2.250  }
   2.251  
   2.252  extern "C" size_t Android_JNI_FileWrite(SDL_RWops* ctx, const void* buffer,
   2.253 @@ -727,16 +786,28 @@
   2.254              mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.fileNameRef);
   2.255          }
   2.256  
   2.257 -        jobject inputStream = (jobject)ctx->hidden.androidio.inputStreamRef;
   2.258 +        if (ctx->hidden.androidio.assetFileDescriptorRef) {
   2.259 +            jobject inputStream = (jobject)ctx->hidden.androidio.assetFileDescriptorRef;
   2.260 +            jmethodID mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
   2.261 +                    "close", "()V");
   2.262 +            mEnv->CallVoidMethod(inputStream, mid);
   2.263 +            mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.assetFileDescriptorRef);
   2.264 +            if (Android_JNI_ExceptionOccurred()) {
   2.265 +                result = -1;
   2.266 +            }
   2.267 +        }
   2.268 +        else {
   2.269 +            jobject inputStream = (jobject)ctx->hidden.androidio.inputStreamRef;
   2.270  
   2.271 -        // inputStream.close();
   2.272 -        jmethodID mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
   2.273 -                "close", "()V");
   2.274 -        mEnv->CallVoidMethod(inputStream, mid);
   2.275 -        mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.inputStreamRef);
   2.276 -        mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.readableByteChannelRef);
   2.277 -        if (Android_JNI_ExceptionOccurred()) {
   2.278 -            result = -1;
   2.279 +            // inputStream.close();
   2.280 +            jmethodID mid = mEnv->GetMethodID(mEnv->GetObjectClass(inputStream),
   2.281 +                    "close", "()V");
   2.282 +            mEnv->CallVoidMethod(inputStream, mid);
   2.283 +            mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.inputStreamRef);
   2.284 +            mEnv->DeleteGlobalRef((jobject)ctx->hidden.androidio.readableByteChannelRef);
   2.285 +            if (Android_JNI_ExceptionOccurred()) {
   2.286 +                result = -1;
   2.287 +            }
   2.288          }
   2.289  
   2.290          if (release) {
   2.291 @@ -755,60 +826,86 @@
   2.292  
   2.293  extern "C" Sint64 Android_JNI_FileSeek(SDL_RWops* ctx, Sint64 offset, int whence)
   2.294  {
   2.295 -    Sint64 newPosition;
   2.296 +    if (ctx->hidden.androidio.assetFileDescriptorRef) {
   2.297 +        switch (whence) {
   2.298 +            case RW_SEEK_SET:
   2.299 +                if (ctx->hidden.androidio.size != -1 /*UNKNOWN_LENGTH*/ && offset > ctx->hidden.androidio.size) offset = ctx->hidden.androidio.size;
   2.300 +                offset += ctx->hidden.androidio.offset;
   2.301 +                break;
   2.302 +            case RW_SEEK_CUR:
   2.303 +                offset += ctx->hidden.androidio.position;
   2.304 +                if (ctx->hidden.androidio.size != -1 /*UNKNOWN_LENGTH*/ && offset > ctx->hidden.androidio.size) offset = ctx->hidden.androidio.size;
   2.305 +                offset += ctx->hidden.androidio.offset;
   2.306 +                break;
   2.307 +            case RW_SEEK_END:
   2.308 +                offset = ctx->hidden.androidio.offset + ctx->hidden.androidio.size + offset;
   2.309 +                break;
   2.310 +            default:
   2.311 +                SDL_SetError("Unknown value for 'whence'");
   2.312 +                return -1;
   2.313 +        }
   2.314 +        whence = SEEK_SET;
   2.315  
   2.316 -    switch (whence) {
   2.317 -        case RW_SEEK_SET:
   2.318 -            newPosition = offset;
   2.319 -            break;
   2.320 -        case RW_SEEK_CUR:
   2.321 -            newPosition = ctx->hidden.androidio.position + offset;
   2.322 -            break;
   2.323 -        case RW_SEEK_END:
   2.324 -            newPosition = ctx->hidden.androidio.size + offset;
   2.325 -            break;
   2.326 -        default:
   2.327 -            SDL_SetError("Unknown value for 'whence'");
   2.328 +        off_t ret = lseek(ctx->hidden.androidio.fd, (off_t)offset, SEEK_SET);
   2.329 +        if (ret == -1) return -1;
   2.330 +        ctx->hidden.androidio.position = ret - ctx->hidden.androidio.offset;
   2.331 +    } else {
   2.332 +        Sint64 newPosition;
   2.333 +
   2.334 +        switch (whence) {
   2.335 +            case RW_SEEK_SET:
   2.336 +                newPosition = offset;
   2.337 +                break;
   2.338 +            case RW_SEEK_CUR:
   2.339 +                newPosition = ctx->hidden.androidio.position + offset;
   2.340 +                break;
   2.341 +            case RW_SEEK_END:
   2.342 +                newPosition = ctx->hidden.androidio.size + offset;
   2.343 +                break;
   2.344 +            default:
   2.345 +                SDL_SetError("Unknown value for 'whence'");
   2.346 +                return -1;
   2.347 +        }
   2.348 +
   2.349 +        /* Validate the new position */
   2.350 +        if (newPosition < 0) {
   2.351 +            SDL_Error(SDL_EFSEEK);
   2.352              return -1;
   2.353 -    }
   2.354 +        }
   2.355 +        if (newPosition > ctx->hidden.androidio.size) {
   2.356 +            newPosition = ctx->hidden.androidio.size;
   2.357 +        }
   2.358  
   2.359 -    /* Validate the new position */
   2.360 -    if (newPosition < 0) {
   2.361 -        SDL_Error(SDL_EFSEEK);
   2.362 -        return -1;
   2.363 -    }
   2.364 -    if (newPosition > ctx->hidden.androidio.size) {
   2.365 -        newPosition = ctx->hidden.androidio.size;
   2.366 -    }
   2.367 +        Sint64 movement = newPosition - ctx->hidden.androidio.position;
   2.368 +        if (movement > 0) {
   2.369 +            unsigned char buffer[4096];
   2.370  
   2.371 -    Sint64 movement = newPosition - ctx->hidden.androidio.position;
   2.372 -    if (movement > 0) {
   2.373 -        unsigned char buffer[4096];
   2.374 +            // The easy case where we're seeking forwards
   2.375 +            while (movement > 0) {
   2.376 +                Sint64 amount = sizeof (buffer);
   2.377 +                if (amount > movement) {
   2.378 +                    amount = movement;
   2.379 +                }
   2.380 +                size_t result = Android_JNI_FileRead(ctx, buffer, 1, amount);
   2.381 +                if (result <= 0) {
   2.382 +                    // Failed to read/skip the required amount, so fail
   2.383 +                    return -1;
   2.384 +                }
   2.385  
   2.386 -        // The easy case where we're seeking forwards
   2.387 -        while (movement > 0) {
   2.388 -            Sint64 amount = sizeof (buffer);
   2.389 -            if (amount > movement) {
   2.390 -                amount = movement;
   2.391 -            }
   2.392 -            size_t result = Android_JNI_FileRead(ctx, buffer, 1, amount);
   2.393 -            if (result <= 0) {
   2.394 -                // Failed to read/skip the required amount, so fail
   2.395 -                return -1;
   2.396 +                movement -= result;
   2.397              }
   2.398  
   2.399 -            movement -= result;
   2.400 +        } else if (movement < 0) {
   2.401 +            // We can't seek backwards so we have to reopen the file and seek
   2.402 +            // forwards which obviously isn't very efficient
   2.403 +            Android_JNI_FileClose(ctx, false);
   2.404 +            Android_JNI_FileOpen(ctx);
   2.405 +            Android_JNI_FileSeek(ctx, newPosition, RW_SEEK_SET);
   2.406          }
   2.407 -
   2.408 -    } else if (movement < 0) {
   2.409 -        // We can't seek backwards so we have to reopen the file and seek
   2.410 -        // forwards which obviously isn't very efficient
   2.411 -        Android_JNI_FileClose(ctx, false);
   2.412 -        Android_JNI_FileOpen(ctx);
   2.413 -        Android_JNI_FileSeek(ctx, newPosition, RW_SEEK_SET);
   2.414      }
   2.415  
   2.416      return ctx->hidden.androidio.position;
   2.417 +    
   2.418  }
   2.419  
   2.420  extern "C" int Android_JNI_FileClose(SDL_RWops* ctx)