0

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();
    }
AggieDev
  • 5,015
  • 9
  • 26
  • 48
  • 2
    You'd need to switch to some PDF generation library that streams the output, rather than builds the whole document in heap space. – CommonsWare Jan 06 '15 at 21:45
  • Are there any ways to temporarily store parts of the string and unallocate them from the heap to prevent the crash as suggested [here?](http://stackoverflow.com/a/11275719/2082140) There are several reasons I have to stick to this Android library after much research of the PDF libraries out there. – AggieDev Jan 06 '15 at 21:47
  • Only by modifying the library. You aren't the one doing the allocation -- the library is. – CommonsWare Jan 06 '15 at 21:54
  • To address this, I've added an edit to the bottom of the question, is there a way to change this part of the library to fix this issue? – AggieDev Jan 06 '15 at 22:06
  • Look at this answer it will help you a lot. http://stackoverflow.com/questions/27549232/outofmemory-recycle-images/27555881#27555881 – Josef Jan 06 '15 at 22:11
  • As I said in the question, I'm already using android:largeHeap="true" – AggieDev Jan 06 '15 at 22:25
  • *is there a way to change this part of the library to fix this issue* - the whole architecture of that APW library is extremely memory consuming, not only the part you posted. For an acceptable memory footprint you'd have to fully re-design it. – mkl Jan 07 '15 at 05:43

1 Answers1

-1

One possible Solution for this issue is to avoid the window leak.

Window leak occurs when you hold a reference to the Activity context even after the activity is destroyed or even after a new instance of the activity is created.

In your case it is the context of MainActivity. To resolve this, you can try the following.

Instead of

 View dialogLayout = ((MainActivity)context).getLayoutInflater().inflate(R.layout.progress_dialog, null);

try

 View dialogLayout = (context.getLayoutInflater().inflate(R.layout.progress_dialog, null);
Prem
  • 4,823
  • 4
  • 31
  • 63
  • This doesn't makes sense as Context doesn't have the method getLayoutInflater(). Also, isn't this still using a reference to the Activity context? Lastly the window leak occurs because of the line before this where the dialog is shown, the layout inflation doesn't cause the leak. – AggieDev Jan 07 '15 at 03:23