I have an app that allows the user to create PDF documents from images. This works fine (though a bit slow, roughly 3 seconds of waiting per page) unless the document is around 10 pages or more, which then after around 30 seconds of waiting the app will crash with the following stacktrace:
01-06 15:28:32.430 27558-27780/appuccino.simplyscan E/art﹕ Throwing OutOfMemoryError "Failed to allocate a 58363924 byte allocation with 16777216 free bytes and 53MB until OOM"
--------- beginning of crash
01-06 15:28:32.442 27558-27780/appuccino.simplyscan E/AndroidRuntime﹕ FATAL EXCEPTION: AsyncTask #1
Process: appuccino.simplyscan, PID: 27558
java.lang.RuntimeException: An error occured while executing doInBackground()
at android.os.AsyncTask$3.done(AsyncTask.java:300)
at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:355)
at java.util.concurrent.FutureTask.setException(FutureTask.java:222)
at java.util.concurrent.FutureTask.run(FutureTask.java:242)
at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
at java.lang.Thread.run(Thread.java:818)
Caused by: java.lang.OutOfMemoryError: Failed to allocate a 58363924 byte allocation with 16777216 free bytes and 53MB until OOM
at java.lang.AbstractStringBuilder.enlargeBuffer(AbstractStringBuilder.java:95)
at java.lang.AbstractStringBuilder.append0(AbstractStringBuilder.java:146)
at java.lang.StringBuilder.append(StringBuilder.java:216)
at appuccino.simplyscan.PDFWriter.List.renderList(List.java:24)
at appuccino.simplyscan.PDFWriter.Body.render(Body.java:88)
at appuccino.simplyscan.PDFWriter.Body.toPDFString(Body.java:93)
at appuccino.simplyscan.PDFWriter.PDFDocument.toPDFString(PDFDocument.java:57)
at appuccino.simplyscan.PDFWriter.PDFWriter.asString(PDFWriter.java:129)
at appuccino.simplyscan.AsyncTasks.PDFZIPAsyncTask$PDFAsyncTask.doInBackground(PDFZIPAsyncTask.java:99)
at appuccino.simplyscan.AsyncTasks.PDFZIPAsyncTask$PDFAsyncTask.doInBackground(PDFZIPAsyncTask.java:36)
at android.os.AsyncTask$2.call(AsyncTask.java:288)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
at java.lang.Thread.run(Thread.java:818)
01-06 15:28:33.200 27558-27558/appuccino.simplyscan E/WindowManager﹕ android.view.WindowLeaked: Activity appuccino.simplyscan.Activities.MainActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView{1ccc212f V.E..... R......D 0,0-1026,441} that was originally added here
at android.view.ViewRootImpl.<init>(ViewRootImpl.java:363)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:261)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
at android.app.Dialog.show(Dialog.java:298)
at android.app.ProgressDialog.show(ProgressDialog.java:116)
at android.app.ProgressDialog.show(ProgressDialog.java:104)
at appuccino.simplyscan.AsyncTasks.PDFZIPAsyncTask$PDFAsyncTask.<init>(PDFZIPAsyncTask.java:52)
at appuccino.simplyscan.Extra.DocumentAdapter.startPDFCreateAsyncTask(DocumentAdapter.java:255)
at appuccino.simplyscan.Extra.DocumentAdapter.access$100(DocumentAdapter.java:36)
at appuccino.simplyscan.Extra.DocumentAdapter$4.onClick(DocumentAdapter.java:181)
at com.android.internal.app.AlertController$AlertParams$3.onItemClick(AlertController.java:1017)
at android.widget.AdapterView.performItemClick(AdapterView.java:300)
at android.widget.AbsListView.performItemClick(AbsListView.java:1143)
at android.widget.AbsListView$PerformClick.run(AbsListView.java:3044)
at android.widget.AbsListView$3.run(AbsListView.java:3833)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5221)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
Though it points to a window leak in the stack trace, after looking at it you can see its from an OutOfMemory error in the app. Currently I have android:largeHeap="true"
in the Manifest which helped greatly with this, as previously the app would crash with a 3 page document.
Is there a way to change my code to allow a user to create a large-paged document, even if they have to wait, without making the app crash with an OutOfMemory error? My PDF creation is based on the PDFWriter Android library (which can be found here in case you want to follow the stack trace into the library's code). Here is my AsyncTask class that implements the PDF creation that causes the crash. Notice the comments I left for which lines the stack trace points to:
public static class PDFAsyncTask extends AsyncTask<Document, Integer, Boolean> {
Context context;
DocumentAdapter documentAdapter;
Document document;
File pdfPath;
ProgressDialog dialog;
PreviewActivity previewActivity;
public PDFAsyncTask() {
}
public PDFAsyncTask(Context context, DocumentAdapter documentAdapter) {
this.documentAdapter = documentAdapter;
this.context = context;
previewActivity = null;
//THIS IS THE LINE THAT THE WINDOW LEAK IN THE STACKTRACE POINTS TO
dialog = ProgressDialog.show(context, "Converting to PDF...", null, true, true);
View dialogLayout = ((MainActivity)context).getLayoutInflater().inflate(R.layout.progress_dialog, null);
dialog.setContentView(dialogLayout);
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
cancel(true);
}
});
}
public PDFAsyncTask(Context context, PreviewActivity previewActivity) {
this.previewActivity = previewActivity;
this.context = context;
documentAdapter = null;
dialog = ProgressDialog.show(context, "Converting to PDF...", null, true, false);
View dialogLayout = ((Activity)context).getLayoutInflater().inflate(R.layout.progress_dialog, null);
dialog.setContentView(dialogLayout);
}
@Override
protected Boolean doInBackground(Document... docs) {
document = docs[0];
PDFWriter pdfWriter = new PDFWriter(PaperSize.A4_WIDTH, PaperSize.A4_HEIGHT);
for(int i = 0; i < document.getBitmapList().size(); i++){
Bitmap bitmap = document.getBitmapList().get(i);
//resize bitmap so that it fits in document
Bitmap resizedBitmap = resizeBitmapForPDF(bitmap, PaperSize.A4_WIDTH, PaperSize.A4_HEIGHT);
//find out side or top margin of image so that it is centered
int sideMargin = 0, topMargin = 0;
//there is margin on top and bottom
if(resizedBitmap.getHeight() < PaperSize.A4_HEIGHT){
float halfOfDoc = PaperSize.A4_HEIGHT / 2.0f;
topMargin = Math.round(halfOfDoc - ((float)resizedBitmap.getHeight() / 2.0f));
}
//there is margin on left and right
else if (resizedBitmap.getWidth() < PaperSize.A4_WIDTH){
float halfOfDoc = PaperSize.A4_WIDTH / 2.0f;
sideMargin = Math.round(halfOfDoc - ((float)resizedBitmap.getWidth() / 2.0f));
}
pdfWriter.addImageKeepRatio(sideMargin, topMargin, resizedBitmap.getWidth(), resizedBitmap.getHeight(), bitmap);
if(i != document.getBitmapList().size() - 1)
pdfWriter.newPage();
}
//THIS IS THE LINE THE STACK TRACE POINTS TO FOR THE OUTOFMEMORY ERROR FROM THE STRINGBUILDER
String pdfAsString = pdfWriter.asString();
String externalStorageRoot = Environment.getExternalStorageDirectory().toString();
File pdfDirectory = new File(externalStorageRoot + "/SimplyScan/Exports");
pdfDirectory.mkdirs();
pdfPath = new File(pdfDirectory, document.getName() + ".pdf");
try {
FileOutputStream pdfFile = new FileOutputStream(pdfPath);
pdfFile.write(pdfAsString.getBytes("ISO-8859-1"));
pdfFile.close();
return true;
} catch(Exception e) {
e.printStackTrace();
return false;
}
}
/*
First resize to if width is over maxWidth, then resize if height is over maxHeight
*/
private Bitmap resizeBitmapForPDF(Bitmap bitmap, int maxWidth, int maxHeight) {
int newWidth, newHeight;
Bitmap compressedBitmap = Bitmap.createBitmap(bitmap);
//if width greater than maxWidth, match the maxWidth and scale the height
if(compressedBitmap.getWidth() > maxWidth){
newWidth = maxWidth;
newHeight = Math.round((float) compressedBitmap.getHeight() * (float) maxWidth / (float) compressedBitmap.getWidth());
compressedBitmap = Bitmap.createScaledBitmap(compressedBitmap, newWidth, newHeight, true);
}
//if height greater than maxHeight, match the maxHeight and scale the width
if(compressedBitmap.getHeight() > maxHeight){
newHeight = maxHeight;
newWidth = Math.round((float)compressedBitmap.getWidth() * (float)maxHeight / (float)compressedBitmap.getHeight());
compressedBitmap = Bitmap.createScaledBitmap(compressedBitmap, newWidth, newHeight, true);
}
return compressedBitmap;
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
dialog.dismiss();
//success from documentAdapter
if(result && documentAdapter != null){
documentAdapter.showSendOrViewDialog(document, pdfPath);
} else if (result && previewActivity != null){ //success from PreviewActivity
previewActivity.showPDFSendOrViewDialog(document, pdfPath);
} else {
Toast.makeText(context, "PDF creation failed, please try again", Toast.LENGTH_LONG).show();
}
}
}
EDIT: As a possible way to fix this issue, is there a way to change this part of the library to not put all memory directly on the heap? This is the part of the library that the stack trace is pointing to in PDFDocument
:
@Override
public String toPDFString() {
StringBuilder sb = new StringBuilder();
sb.append(mHeader.toPDFString());
sb.append(mBody.toPDFString());
mCRT.setObjectNumberStart(mBody.getObjectNumberStart());
int x = 0;
while (x < mBody.getObjectsCount()) {
IndirectObject iobj = mBody.getObjectByNumberID(++x);
if (iobj != null) {
mCRT.addObjectXRefInfo(iobj.getByteOffset(), iobj.getGeneration(), iobj.getInUse());
}
}
mTrailer.setObjectsCount(mBody.getObjectsCount());
mTrailer.setCrossReferenceTableByteOffset(sb.length());
mTrailer.setId(Indentifiers.generateId());
return sb.toString() + mCRT.toPDFString() + mTrailer.toPDFString();
}