From 0cbf73e579846395c7b9f7ebd3f3f229d99104ac Mon Sep 17 00:00:00 2001 From: Robin Watts Date: Thu, 24 Jan 2013 19:26:26 +0000 Subject: Android: Fix NullPointerException seen on Google Play According to Google Plays automated crash detection, we get a NullPointerException when trying to parse a null string as a Uri. This turns out to be caused by us trying to open a PDF attachment from gmail. This is because MuPDF is invoked with a content:// URL that does not have a file associated with it. Instead we can open that URL as an InputStream. Here we amend MuPDF to spot that case, and to open the InputStream, suck the data into a byteArray, and then to use that to open the file from. --- android/jni/mupdf.c | 131 ++++++++++++++++++++- android/res/values/strings.xml | 3 +- .../src/com/artifex/mupdfdemo/MuPDFActivity.java | 74 +++++++++++- android/src/com/artifex/mupdfdemo/MuPDFCore.java | 12 ++ 4 files changed, 213 insertions(+), 7 deletions(-) (limited to 'android') diff --git a/android/jni/mupdf.c b/android/jni/mupdf.c index ecb9f20b..a4180e54 100644 --- a/android/jni/mupdf.c +++ b/android/jni/mupdf.c @@ -13,6 +13,7 @@ #endif #include "fitz.h" +#include "fitz-internal.h" #include "mupdf.h" #define JNI_FN(A) Java_com_artifex_mupdfdemo_ ## A @@ -89,9 +90,15 @@ struct globals_s int alert_reply; pthread_cond_t alert_request_cond; pthread_cond_t alert_reply_cond; + + // For the buffer reading mode, we need to implement stream reading, which + // needs access to the following. + JNIEnv *env; + jclass thiz; }; static jfieldID global_fid; +static jfieldID buffer_fid; static void drop_page_cache(globals *glo, page_cache *pc) { @@ -225,7 +232,10 @@ static void alerts_fin(globals *glo) static globals *get_globals(JNIEnv *env, jobject thiz) { - return (globals *)(void *)((*env)->GetLongField(env, thiz, global_fid)); + globals *glo = (globals *)(void *)((*env)->GetLongField(env, thiz, global_fid)); + glo->env = env; + glo->thiz = thiz; + return glo; } JNIEXPORT jlong JNICALL @@ -281,7 +291,7 @@ JNI_FN(MuPDFCore_openFile)(JNIEnv * env, jobject thiz, jstring jfilename) } fz_catch(ctx) { - fz_throw(ctx, "Cannot open document: '%s'\n", filename); + fz_throw(ctx, "Cannot open document: '%s'", filename); } LOGE("Done!"); } @@ -301,6 +311,123 @@ JNI_FN(MuPDFCore_openFile)(JNIEnv * env, jobject thiz, jstring jfilename) return (jlong)(void *)glo; } +static int bufferStreamRead(fz_stream *stream, unsigned char *buf, int len) +{ + globals *glo = (globals *)stream->state; + JNIEnv *env = glo->env; + jbyteArray array = (jbyteArray)(void *)((*env)->GetObjectField(env, glo->thiz, buffer_fid)); + int arrayLength = (*env)->GetArrayLength(env, array); + + if (stream->pos > arrayLength) + stream->pos = arrayLength; + if (stream->pos < 0) + stream->pos = 0; + if (len + stream->pos > arrayLength) + len = arrayLength - stream->pos; + + (*env)->GetByteArrayRegion(env, array, stream->pos, len, buf); + (*env)->DeleteLocalRef(env, array); + return len; +} + +static void bufferStreamClose(fz_context *ctx, void *state) +{ + /* Nothing to do */ +} + +static void bufferStreamSeek(fz_stream *stream, int offset, int whence) +{ + globals *glo = (globals *)stream->state; + JNIEnv *env = glo->env; + jbyteArray array = (jbyteArray)(void *)((*env)->GetObjectField(env, glo->thiz, buffer_fid)); + int arrayLength = (*env)->GetArrayLength(env, array); + + (*env)->DeleteLocalRef(env, array); + + if (whence == 0) /* SEEK_SET */ + stream->pos = offset; + else if (whence == 1) /* SEEK_CUR */ + stream->pos += offset; + else if (whence == 2) /* SEEK_END */ + stream->pos = arrayLength + offset; + + if (stream->pos > arrayLength) + stream->pos = arrayLength; + if (stream->pos < 0) + stream->pos = 0; + + stream->rp = stream->bp; + stream->wp = stream->bp; +} + +JNIEXPORT jlong JNICALL +JNI_FN(MuPDFCore_openBuffer)(JNIEnv * env, jobject thiz) +{ + globals *glo; + fz_context *ctx; + jclass clazz; + fz_stream *stream; + +#ifdef NDK_PROFILER + monstartup("libmupdf.so"); +#endif + + clazz = (*env)->GetObjectClass(env, thiz); + global_fid = (*env)->GetFieldID(env, clazz, "globals", "J"); + + glo = calloc(1, sizeof(*glo)); + if (glo == NULL) + return 0; + glo->resolution = 160; + glo->alerts_initialised = 0; + glo->env = env; + glo->thiz = thiz; + buffer_fid = (*env)->GetFieldID(env, clazz, "fileBuffer", "[B"); + + /* 128 MB store for low memory devices. Tweak as necessary. */ + glo->ctx = ctx = fz_new_context(NULL, NULL, 128 << 20); + if (!ctx) + { + LOGE("Failed to initialise context"); + free(glo); + return 0; + } + + glo->doc = NULL; + fz_try(ctx) + { + stream = fz_new_stream(ctx, glo, bufferStreamRead, bufferStreamClose); + stream->seek = bufferStreamSeek; + + glo->colorspace = fz_device_rgb; + + LOGE("Opening document..."); + fz_try(ctx) + { + glo->current_path = NULL; + glo->doc = fz_open_document_with_stream(ctx, "", stream); + alerts_init(glo); + } + fz_catch(ctx) + { + fz_throw(ctx, "Cannot open memory document"); + } + LOGE("Done!"); + } + fz_catch(ctx) + { + LOGE("Failed: %s", ctx->error->message); + fz_close_document(glo->doc); + glo->doc = NULL; + fz_free_context(ctx); + glo->ctx = NULL; + free(glo); + glo = NULL; + } + + return (jlong)(void *)glo; +} + JNIEXPORT int JNICALL JNI_FN(MuPDFCore_countPagesInternal)(JNIEnv *env, jobject thiz) { diff --git a/android/res/values/strings.xml b/android/res/values/strings.xml index 72794bcc..1468d963 100644 --- a/android/res/values/strings.xml +++ b/android/res/values/strings.xml @@ -1,10 +1,11 @@ MuPDF - 1.1 + 1.1 (Build 6) Storage Media not present Sharing the storage media with a PC can make it inaccessible Unable to open document + %1$s: %2$s Cancel Search backwards Search forwards diff --git a/android/src/com/artifex/mupdfdemo/MuPDFActivity.java b/android/src/com/artifex/mupdfdemo/MuPDFActivity.java index f41b6178..f095a041 100644 --- a/android/src/com/artifex/mupdfdemo/MuPDFActivity.java +++ b/android/src/com/artifex/mupdfdemo/MuPDFActivity.java @@ -1,6 +1,8 @@ package com.artifex.mupdfdemo; import java.util.concurrent.Executor; +import java.io.InputStream; +import java.io.FileInputStream; import android.animation.Animator; import android.animation.AnimatorInflater; @@ -12,6 +14,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.content.res.Resources; import android.database.Cursor; import android.graphics.Color; import android.graphics.RectF; @@ -260,6 +263,23 @@ public class MuPDFActivity extends Activity return core; } + private MuPDFCore openBuffer(byte buffer[]) + { + System.out.println("Trying to open byte buffer"); + try + { + core = new MuPDFCore(buffer); + // New file: drop the old outline data + OutlineActivityData.set(null); + } + catch (Exception e) + { + System.out.println(e); + return null; + } + return core; + } + /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) @@ -292,6 +312,7 @@ public class MuPDFActivity extends Activity } if (core == null) { Intent intent = getIntent(); + byte buffer[] = null; if (Intent.ACTION_VIEW.equals(intent.getAction())) { Uri uri = intent.getData(); if (uri.toString().startsWith("content://")) { @@ -300,10 +321,51 @@ public class MuPDFActivity extends Activity // using explicit paths. Cursor cursor = getContentResolver().query(uri, new String[]{"_data"}, null, null, null); if (cursor.moveToFirst()) { - uri = Uri.parse(cursor.getString(0)); + String str = cursor.getString(0); + String failString = null; + if (str == null) { + try { + InputStream is = getContentResolver().openInputStream(uri); + int len = is.available(); + buffer = new byte[len]; + is.read(buffer, 0, len); + is.close(); + } + catch (java.lang.OutOfMemoryError e) + { + System.out.println("Out of memory during buffer reading"); + failString = e.toString(); + } + catch (Exception e) { + failString = e.toString(); + } + if (failString != null) + { + buffer = null; + Resources res = getResources(); + AlertDialog alert = mAlertBuilder.create(); + String contentFailure = res.getString(R.string.content_failure); + String openFailed = res.getString(R.string.open_failed); + setTitle(String.format(contentFailure, openFailed, failString)); + alert.setButton(AlertDialog.BUTTON_POSITIVE, "Dismiss", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + finish(); + } + }); + alert.show(); + return; + } + } else { + uri = Uri.parse(str); + } } } - core = openFile(Uri.decode(uri.getEncodedPath())); + if (buffer != null) { + core = openBuffer(buffer); + } else { + core = openFile(Uri.decode(uri.getEncodedPath())); + } SearchTaskResult.set(null); } if (core != null && core.needsPassword()) { @@ -1023,17 +1085,21 @@ public class MuPDFActivity extends Activity @Override protected void onStart() { if (core != null) + { core.startAlerts(); + createAlertWaiter(); + } - createAlertWaiter(); super.onStart(); } @Override protected void onStop() { - destroyAlertWaiter(); if (core != null) + { + destroyAlertWaiter(); core.stopAlerts(); + } super.onStop(); } diff --git a/android/src/com/artifex/mupdfdemo/MuPDFCore.java b/android/src/com/artifex/mupdfdemo/MuPDFCore.java index cf35be8d..ed9ded92 100644 --- a/android/src/com/artifex/mupdfdemo/MuPDFCore.java +++ b/android/src/com/artifex/mupdfdemo/MuPDFCore.java @@ -18,9 +18,11 @@ public class MuPDFCore private float pageWidth; private float pageHeight; private long globals; + private byte fileBuffer[]; /* The native functions */ private native long openFile(String filename); + private native long openBuffer(); private native int countPagesInternal(); private native void gotoPageInternal(int localActionPageNum); private native float getPageWidth(); @@ -68,6 +70,16 @@ public class MuPDFCore } } + public MuPDFCore(byte buffer[]) throws Exception + { + fileBuffer = buffer; + globals = openBuffer(); + if (globals == 0) + { + throw new Exception("Failed to open buffer"); + } + } + public int countPages() { if (numPages < 0) -- cgit v1.2.3