diff options
13 files changed, 701 insertions, 49 deletions
diff --git a/core/jni/Android.mk b/core/jni/Android.mk index 2106d38..dbaa4b8 100644 --- a/core/jni/Android.mk +++ b/core/jni/Android.mk @@ -129,6 +129,7 @@ LOCAL_SRC_FILES:= \ android/graphics/Xfermode.cpp \ android/graphics/YuvToJpegEncoder.cpp \ android/graphics/pdf/PdfDocument.cpp \ + android/graphics/pdf/PdfEditor.cpp \ android/graphics/pdf/PdfRenderer.cpp \ android_media_AudioRecord.cpp \ android_media_AudioSystem.cpp \ diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index 62d8036..a63258c 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -126,6 +126,7 @@ extern int register_android_graphics_Region(JNIEnv* env); extern int register_android_graphics_SurfaceTexture(JNIEnv* env); extern int register_android_graphics_Xfermode(JNIEnv* env); extern int register_android_graphics_pdf_PdfDocument(JNIEnv* env); +extern int register_android_graphics_pdf_PdfEditor(JNIEnv* env); extern int register_android_graphics_pdf_PdfRenderer(JNIEnv* env); extern int register_android_view_DisplayEventReceiver(JNIEnv* env); extern int register_android_view_RenderNode(JNIEnv* env); @@ -1305,6 +1306,7 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_graphics_Xfermode), REG_JNI(register_android_graphics_YuvImage), REG_JNI(register_android_graphics_pdf_PdfDocument), + REG_JNI(register_android_graphics_pdf_PdfEditor), REG_JNI(register_android_graphics_pdf_PdfRenderer), REG_JNI(register_android_database_CursorWindow), diff --git a/core/jni/android/graphics/pdf/PdfEditor.cpp b/core/jni/android/graphics/pdf/PdfEditor.cpp new file mode 100644 index 0000000..5f60c9e --- /dev/null +++ b/core/jni/android/graphics/pdf/PdfEditor.cpp @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "jni.h" +#include "JNIHelp.h" +#include "fpdfview.h" +#include "fpdfedit.h" +#include "fpdfsave.h" + +#include <android_runtime/AndroidRuntime.h> +#include <vector> +#include <utils/Log.h> +#include <unistd.h> +#include <sys/types.h> +#include <unistd.h> + +namespace android { + +static Mutex sLock; + +static int sUnmatchedInitRequestCount = 0; + +static void initializeLibraryIfNeeded() { + Mutex::Autolock _l(sLock); + if (sUnmatchedInitRequestCount == 0) { + FPDF_InitLibrary(NULL); + } + sUnmatchedInitRequestCount++; +} + +static void destroyLibraryIfNeeded() { + Mutex::Autolock _l(sLock); + sUnmatchedInitRequestCount--; + if (sUnmatchedInitRequestCount == 0) { + FPDF_DestroyLibrary(); + } +} + +static int getBlock(void* param, unsigned long position, unsigned char* outBuffer, + unsigned long size) { + const int fd = reinterpret_cast<intptr_t>(param); + const int readCount = pread(fd, outBuffer, size, position); + if (readCount < 0) { + ALOGE("Cannot read from file descriptor. Error:%d", errno); + return 0; + } + return 1; +} + +static jlong nativeOpen(JNIEnv* env, jclass thiz, jint fd, jlong size) { + initializeLibraryIfNeeded(); + + FPDF_FILEACCESS loader; + loader.m_FileLen = size; + loader.m_Param = reinterpret_cast<void*>(intptr_t(fd)); + loader.m_GetBlock = &getBlock; + + FPDF_DOCUMENT document = FPDF_LoadCustomDocument(&loader, NULL); + + if (!document) { + const long error = FPDF_GetLastError(); + jniThrowException(env, "java/io/IOException", + "cannot create document. Error:" + error); + destroyLibraryIfNeeded(); + return -1; + } + + return reinterpret_cast<jlong>(document); +} + +static void nativeClose(JNIEnv* env, jclass thiz, jlong documentPtr) { + FPDF_DOCUMENT document = reinterpret_cast<FPDF_DOCUMENT>(documentPtr); + FPDF_CloseDocument(document); + destroyLibraryIfNeeded(); +} + +static jint nativeGetPageCount(JNIEnv* env, jclass thiz, jlong documentPtr) { + FPDF_DOCUMENT document = reinterpret_cast<FPDF_DOCUMENT>(documentPtr); + return FPDF_GetPageCount(document); +} + +static jint nativeRemovePage(JNIEnv* env, jclass thiz, jlong documentPtr, jint pageIndex) { + FPDF_DOCUMENT document = reinterpret_cast<FPDF_DOCUMENT>(documentPtr); + FPDFPage_Delete(document, pageIndex); + return FPDF_GetPageCount(document); +} + +struct PdfToFdWriter : FPDF_FILEWRITE { + int dstFd; +}; + +static bool writeAllBytes(const int fd, const void* buffer, const size_t byteCount) { + char* writeBuffer = static_cast<char*>(const_cast<void*>(buffer)); + size_t remainingBytes = byteCount; + while (remainingBytes > 0) { + ssize_t writtenByteCount = write(fd, writeBuffer, remainingBytes); + if (writtenByteCount == -1) { + if (errno == EINTR) { + continue; + } + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, + "Error writing to buffer: %d", errno); + return false; + } + remainingBytes -= writtenByteCount; + writeBuffer += writtenByteCount; + } + return true; +} + +static int writeBlock(FPDF_FILEWRITE* owner, const void* buffer, unsigned long size) { + const PdfToFdWriter* writer = reinterpret_cast<PdfToFdWriter*>(owner); + const bool success = writeAllBytes(writer->dstFd, buffer, size); + if (success < 0) { + ALOGE("Cannot write to file descriptor. Error:%d", errno); + return 0; + } + return 1; +} + +static void nativeWrite(JNIEnv* env, jclass thiz, jlong documentPtr, jint fd) { + FPDF_DOCUMENT document = reinterpret_cast<FPDF_DOCUMENT>(documentPtr); + PdfToFdWriter writer; + writer.dstFd = fd; + writer.WriteBlock = &writeBlock; + const bool success = FPDF_SaveAsCopy(document, &writer, FPDF_NO_INCREMENTAL); + if (!success) { + jniThrowException(env, "java/io/IOException", + "cannot write to fd. Error:" + errno); + destroyLibraryIfNeeded(); + } +} + +static JNINativeMethod gPdfEditor_Methods[] = { + {"nativeOpen", "(IJ)J", (void*) nativeOpen}, + {"nativeClose", "(J)V", (void*) nativeClose}, + {"nativeGetPageCount", "(J)I", (void*) nativeGetPageCount}, + {"nativeRemovePage", "(JI)I", (void*) nativeRemovePage}, + {"nativeWrite", "(JI)V", (void*) nativeWrite} +}; + +int register_android_graphics_pdf_PdfEditor(JNIEnv* env) { + return android::AndroidRuntime::registerNativeMethods( + env, "android/graphics/pdf/PdfEditor", gPdfEditor_Methods, + NELEM(gPdfEditor_Methods)); +}; + +}; diff --git a/graphics/java/android/graphics/pdf/PdfEditor.java b/graphics/java/android/graphics/pdf/PdfEditor.java new file mode 100644 index 0000000..9837139 --- /dev/null +++ b/graphics/java/android/graphics/pdf/PdfEditor.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics.pdf; + +import android.annotation.NonNull; +import android.os.ParcelFileDescriptor; +import android.system.ErrnoException; +import android.system.OsConstants; +import dalvik.system.CloseGuard; +import libcore.io.IoUtils; +import libcore.io.Libcore; + +import java.io.IOException; + +/** + * Class for editing PDF files. + * + * @hide + */ +public final class PdfEditor { + + private final CloseGuard mCloseGuard = CloseGuard.get(); + + private final long mNativeDocument; + + private int mPageCount; + + private ParcelFileDescriptor mInput; + + /** + * Creates a new instance. + * <p> + * <strong>Note:</strong> The provided file descriptor must be <strong>seekable</strong>, + * i.e. its data being randomly accessed, e.g. pointing to a file. After finishing + * with this class you must call {@link #close()}. + * </p> + * <p> + * <strong>Note:</strong> This class takes ownership of the passed in file descriptor + * and is responsible for closing it when the editor is closed. + * </p> + * + * @param input Seekable file descriptor to read from. + * + * @see #close() + */ + public PdfEditor(@NonNull ParcelFileDescriptor input) throws IOException { + if (input == null) { + throw new NullPointerException("input cannot be null"); + } + + final long size; + try { + Libcore.os.lseek(input.getFileDescriptor(), 0, OsConstants.SEEK_SET); + size = Libcore.os.fstat(input.getFileDescriptor()).st_size; + } catch (ErrnoException ee) { + throw new IllegalArgumentException("file descriptor not seekable"); + } + + mInput = input; + mNativeDocument = nativeOpen(mInput.getFd(), size); + mPageCount = nativeGetPageCount(mNativeDocument); + mCloseGuard.open("close"); + } + + /** + * Gets the number of pages in the document. + * + * @return The page count. + */ + public int getPageCount() { + throwIfClosed(); + return mPageCount; + } + + /** + * Removes the page with a given index. + * + * @param pageIndex The page to remove. + */ + public void removePage(int pageIndex) { + throwIfClosed(); + throwIfPageNotInDocument(pageIndex); + mPageCount = nativeRemovePage(mNativeDocument, pageIndex); + } + + /** + * Writes the PDF file to the provided destination. + * <p> + * <strong>Note:</strong> This method takes ownership of the passed in file + * descriptor and is responsible for closing it when writing completes. + * </p> + * @param output The destination. + */ + public void write(ParcelFileDescriptor output) throws IOException { + try { + throwIfClosed(); + nativeWrite(mNativeDocument, output.getFd()); + } finally { + IoUtils.closeQuietly(output); + } + } + + /** + * Closes this editor. You should not use this instance + * after this method is called. + */ + public void close() { + throwIfClosed(); + doClose(); + } + + @Override + protected void finalize() throws Throwable { + try { + mCloseGuard.warnIfOpen(); + if (mInput != null) { + doClose(); + } + } finally { + super.finalize(); + } + } + + private void doClose() { + nativeClose(mNativeDocument); + IoUtils.closeQuietly(mInput); + mInput = null; + mCloseGuard.close(); + } + + private void throwIfClosed() { + if (mInput == null) { + throw new IllegalStateException("Already closed"); + } + } + + private void throwIfPageNotInDocument(int pageIndex) { + if (pageIndex < 0 || pageIndex >= mPageCount) { + throw new IllegalArgumentException("Invalid page index"); + } + } + + private static native long nativeOpen(int fd, long size); + private static native void nativeClose(long documentPtr); + private static native int nativeGetPageCount(long documentPtr); + private static native int nativeRemovePage(long documentPtr, int pageIndex); + private static native void nativeWrite(long documentPtr, int fd); +} diff --git a/graphics/java/android/graphics/pdf/PdfRenderer.java b/graphics/java/android/graphics/pdf/PdfRenderer.java index 1072b3c..359c294 100644 --- a/graphics/java/android/graphics/pdf/PdfRenderer.java +++ b/graphics/java/android/graphics/pdf/PdfRenderer.java @@ -239,7 +239,7 @@ public final class PdfRenderer implements AutoCloseable { } private void throwIfPageNotInDocument(int pageIndex) { - if (pageIndex >= mPageCount) { + if (pageIndex < 0 || pageIndex >= mPageCount) { throw new IllegalArgumentException("Invalid page index"); } } diff --git a/packages/PrintSpooler/Android.mk b/packages/PrintSpooler/Android.mk index c4a55d1..27d1b23 100644 --- a/packages/PrintSpooler/Android.mk +++ b/packages/PrintSpooler/Android.mk @@ -19,7 +19,9 @@ include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := $(call all-java-files-under, src) -LOCAL_SRC_FILES += src/com/android/printspooler/renderer/IPdfRenderer.aidl +LOCAL_SRC_FILES += \ + src/com/android/printspooler/renderer/IPdfRenderer.aidl \ + src/com/android/printspooler/renderer/IPdfEditor.aidl LOCAL_PACKAGE_NAME := PrintSpooler diff --git a/packages/PrintSpooler/AndroidManifest.xml b/packages/PrintSpooler/AndroidManifest.xml index 9efda2f..adff596 100644 --- a/packages/PrintSpooler/AndroidManifest.xml +++ b/packages/PrintSpooler/AndroidManifest.xml @@ -54,7 +54,7 @@ </service> <service - android:name=".renderer.PdfRendererService" + android:name=".renderer.PdfManipulationService" android:isolatedProcess="true"> </service> diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/PageContentRepository.java b/packages/PrintSpooler/src/com/android/printspooler/model/PageContentRepository.java index a581e8a..1d8261b 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/model/PageContentRepository.java +++ b/packages/PrintSpooler/src/com/android/printspooler/model/PageContentRepository.java @@ -37,7 +37,7 @@ import android.util.Log; import android.view.View; import com.android.internal.annotations.GuardedBy; import com.android.printspooler.renderer.IPdfRenderer; -import com.android.printspooler.renderer.PdfRendererService; +import com.android.printspooler.renderer.PdfManipulationService; import com.android.printspooler.util.BitmapSerializeUtils; import dalvik.system.CloseGuard; import libcore.io.IoUtils; @@ -490,7 +490,8 @@ public final class PageContentRepository { new AsyncTask<Void, Void, Integer>() { @Override protected void onPreExecute() { - Intent intent = new Intent(mContext, PdfRendererService.class); + Intent intent = new Intent(PdfManipulationService.ACTION_GET_RENDERER); + intent.setClass(mContext, PdfManipulationService.class); mContext.bindService(intent, AsyncRenderer.this, Context.BIND_AUTO_CREATE); } @@ -555,7 +556,7 @@ public final class PageContentRepository { callback.run(); } } - }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); } public void destroy() { diff --git a/packages/PrintSpooler/src/com/android/printspooler/renderer/IPdfEditor.aidl b/packages/PrintSpooler/src/com/android/printspooler/renderer/IPdfEditor.aidl new file mode 100644 index 0000000..b450ccb --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/renderer/IPdfEditor.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.printspooler.renderer; + +import android.os.ParcelFileDescriptor; +import android.print.PageRange; + +/** + * Interface for communication with a remote pdf editor. + */ +interface IPdfEditor { + int openDocument(in ParcelFileDescriptor source); + void removePages(in PageRange[] pages); + void write(in ParcelFileDescriptor destination); + void closeDocument(); +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/renderer/IPdfRenderer.aidl b/packages/PrintSpooler/src/com/android/printspooler/renderer/IPdfRenderer.aidl index 1fba2b1..8e595d7 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/renderer/IPdfRenderer.aidl +++ b/packages/PrintSpooler/src/com/android/printspooler/renderer/IPdfRenderer.aidl @@ -29,5 +29,4 @@ interface IPdfRenderer { oneway void renderPage(int pageIndex, int bitmapWidth, int bitmapHeight, in PrintAttributes attributes, in ParcelFileDescriptor destination); oneway void closeDocument(); - oneway void writePages(in PageRange[] pages); } diff --git a/packages/PrintSpooler/src/com/android/printspooler/renderer/PdfRendererService.java b/packages/PrintSpooler/src/com/android/printspooler/renderer/PdfManipulationService.java index 4d02c01..62716b2 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/renderer/PdfRendererService.java +++ b/packages/PrintSpooler/src/com/android/printspooler/renderer/PdfManipulationService.java @@ -23,6 +23,7 @@ import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; +import android.graphics.pdf.PdfEditor; import android.graphics.pdf.PdfRenderer; import android.os.IBinder; import android.os.ParcelFileDescriptor; @@ -32,15 +33,21 @@ import android.print.PrintAttributes; import android.print.PrintAttributes.Margins; import android.util.Log; import android.view.View; +import com.android.printspooler.util.PageRangeUtils; import libcore.io.IoUtils; import com.android.printspooler.util.BitmapSerializeUtils; import java.io.IOException; /** - * Service for rendering PDF documents in an isolated process. + * Service for manipulation of PDF documents in an isolated process. */ -public final class PdfRendererService extends Service { - private static final String LOG_TAG = "PdfRendererService"; +public final class PdfManipulationService extends Service { + public static final String ACTION_GET_RENDERER = + "com.android.printspooler.renderer.ACTION_GET_RENDERER"; + public static final String ACTION_GET_EDITOR = + "com.android.printspooler.renderer.ACTION_GET_EDITOR"; + + private static final String LOG_TAG = "PdfManipulationService"; private static final boolean DEBUG = false; private static final int MILS_PER_INCH = 1000; @@ -48,7 +55,18 @@ public final class PdfRendererService extends Service { @Override public IBinder onBind(Intent intent) { - return new PdfRendererImpl(); + String action = intent.getAction(); + switch (action) { + case ACTION_GET_RENDERER: { + return new PdfRendererImpl(); + } + case ACTION_GET_EDITOR: { + return new PdfEditorImpl(); + } + default: { + throw new IllegalArgumentException("Invalid intent action:" + action); + } + } } private final class PdfRendererImpl extends IPdfRenderer.Stub { @@ -60,15 +78,17 @@ public final class PdfRendererService extends Service { @Override public int openDocument(ParcelFileDescriptor source) throws RemoteException { synchronized (mLock) { - throwIfOpened(); - if (DEBUG) { - Log.i(LOG_TAG, "openDocument()"); - } try { + throwIfOpened(); + if (DEBUG) { + Log.i(LOG_TAG, "openDocument()"); + } mRenderer = new PdfRenderer(source); return mRenderer.getPageCount(); - } catch (IOException ioe) { - throw new RemoteException("Cannot open file"); + } catch (IOException|IllegalStateException e) { + IoUtils.closeQuietly(source); + Log.e(LOG_TAG, "Cannot open file", e); + throw new RemoteException(e.toString()); } } } @@ -108,7 +128,7 @@ public final class PdfRendererService extends Service { } matrix.postScale(displayScale, displayScale); - Configuration configuration = PdfRendererService.this.getResources() + Configuration configuration = PdfManipulationService.this.getResources() .getConfiguration(); if (configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { matrix.postTranslate(bitmapWidth - srcWidthPts * displayScale, 0); @@ -147,24 +167,13 @@ public final class PdfRendererService extends Service { synchronized (mLock) { throwIfNotOpened(); if (DEBUG) { - Log.i(LOG_TAG, "openDocument()"); + Log.i(LOG_TAG, "closeDocument()"); } mRenderer.close(); mRenderer = null; } } - @Override - public void writePages(PageRange[] pages) { - synchronized (mLock) { - throwIfNotOpened(); - if (DEBUG) { - Log.i(LOG_TAG, "writePages()"); - } - // TODO: Implement dropping undesired pages. - } - } - private Bitmap getBitmapForSize(int width, int height) { if (mBitmap != null) { if (mBitmap.getWidth() == width && mBitmap.getHeight() == height) { @@ -191,6 +200,91 @@ public final class PdfRendererService extends Service { } } + private final class PdfEditorImpl extends IPdfEditor.Stub { + private final Object mLock = new Object(); + + private PdfEditor mEditor; + + @Override + public int openDocument(ParcelFileDescriptor source) throws RemoteException { + synchronized (mLock) { + try { + throwIfOpened(); + if (DEBUG) { + Log.i(LOG_TAG, "openDocument()"); + } + mEditor = new PdfEditor(source); + return mEditor.getPageCount(); + } catch (IOException|IllegalStateException e) { + IoUtils.closeQuietly(source); + Log.e(LOG_TAG, "Cannot open file", e); + throw new RemoteException(e.toString()); + } + } + } + + @Override + public void removePages(PageRange[] ranges) { + synchronized (mLock) { + throwIfNotOpened(); + if (DEBUG) { + Log.i(LOG_TAG, "removePages()"); + } + + ranges = PageRangeUtils.normalize(ranges); + + final int rangeCount = ranges.length; + for (int i = rangeCount - 1; i >= 0; i--) { + PageRange range = ranges[i]; + for (int j = range.getEnd(); j >= range.getStart(); j--) { + mEditor.removePage(j); + } + } + } + } + + @Override + public void write(ParcelFileDescriptor destination) throws RemoteException { + synchronized (mLock) { + try { + throwIfNotOpened(); + if (DEBUG) { + Log.i(LOG_TAG, "write()"); + } + mEditor.write(destination); + } catch (IOException | IllegalStateException e) { + IoUtils.closeQuietly(destination); + Log.e(LOG_TAG, "Error writing PDF to file.", e); + throw new RemoteException(e.toString()); + } + } + } + + @Override + public void closeDocument() { + synchronized (mLock) { + throwIfNotOpened(); + if (DEBUG) { + Log.i(LOG_TAG, "closeDocument()"); + } + mEditor.close(); + mEditor = null; + } + } + + private void throwIfOpened() { + if (mEditor != null) { + throw new IllegalStateException("Already opened"); + } + } + + private void throwIfNotOpened() { + if (mEditor == null) { + throw new IllegalStateException("Not opened"); + } + } + } + private static int pointsFromMils(int mils) { return (int) (((float) mils / MILS_PER_INCH) * POINTS_IN_INCH); } diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java index 535081f..c517f2d 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java @@ -23,6 +23,7 @@ import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; @@ -30,9 +31,12 @@ import android.content.res.Configuration; import android.database.DataSetObserver; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; import android.print.IPrintDocumentAdapter; import android.print.PageRange; import android.print.PrintAttributes; @@ -74,6 +78,8 @@ import com.android.printspooler.model.PrintSpoolerProvider; import com.android.printspooler.model.PrintSpoolerService; import com.android.printspooler.model.RemotePrintDocument; import com.android.printspooler.model.RemotePrintDocument.RemotePrintDocumentInfo; +import com.android.printspooler.renderer.IPdfEditor; +import com.android.printspooler.renderer.PdfManipulationService; import com.android.printspooler.util.MediaSizeUtils; import com.android.printspooler.util.MediaSizeUtils.MediaSizeComparator; import com.android.printspooler.util.PageRangeUtils; @@ -81,8 +87,15 @@ import com.android.printspooler.util.PrintOptionUtils; import com.android.printspooler.widget.PrintContentView; import com.android.printspooler.widget.PrintContentView.OptionsStateChangeListener; import com.android.printspooler.widget.PrintContentView.OptionsStateController; +import libcore.io.IoUtils; +import libcore.io.Streams; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -186,6 +199,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat private ImageView mPrintButton; private ProgressMessageController mProgressMessageController; + private MutexFileProvider mFileProvider; private MediaSizeComparator mMediaSizeComparator; @@ -257,9 +271,8 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat setTitle(R.string.print_dialog); setContentView(R.layout.print_activity); - final MutexFileProvider fileProvider; try { - fileProvider = new MutexFileProvider( + mFileProvider = new MutexFileProvider( PrintSpoolerService.generateFileForPrintJob( PrintActivity.this, mPrintJob.getId())); } catch (IOException ioe) { @@ -268,12 +281,13 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat } mPrintPreviewController = new PrintPreviewController(PrintActivity.this, - fileProvider); + mFileProvider); mPrintedDocument = new RemotePrintDocument(PrintActivity.this, IPrintDocumentAdapter.Stub.asInterface(documentAdapter), - fileProvider, new RemotePrintDocument.DocumentObserver() { + mFileProvider, new RemotePrintDocument.DocumentObserver() { @Override public void onDestroy() { + setState(STATE_PRINT_CANCELED); finish(); } }, PrintActivity.this); @@ -369,7 +383,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat @Override public void onRequestContentUpdate() { if (canUpdateDocument()) { - updateDocument(true, false); + updateDocument(false); } } @@ -386,7 +400,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat @Override public void onActionPerformed() { if (mState == STATE_UPDATE_FAILED - && canUpdateDocument() && updateDocument(true, true)) { + && canUpdateDocument() && updateDocument(true)) { ensurePreviewUiShown(); setState(STATE_CONFIGURING); updateOptionsUi(); @@ -557,14 +571,13 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat if (resultCode == RESULT_OK && data != null) { setState(STATE_PRINT_COMPLETED); updateOptionsUi(); - Uri uri = data.getData(); - mPrintedDocument.writeContent(getContentResolver(), uri); + final Uri uri = data.getData(); // Calling finish here does not invoke lifecycle callbacks but we // update the print job in onPause if finishing, hence post a message. mDestinationSpinner.post(new Runnable() { @Override public void run() { - finish(); + shredPagesAndFinish(uri); } }); } else if (resultCode == RESULT_CANCELED) { @@ -708,7 +721,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat // Update the content if needed. if (canUpdateDocument()) { - updateDocument(true, false); + updateDocument(false); } } @@ -840,7 +853,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat if (mCurrentPrinter == mDestinationSpinnerAdapter.getPdfPrinter()) { startCreateDocumentActivity(); } else { - finish(); + shredPagesAndFinish(null); } } @@ -893,7 +906,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat attributes.setMinMargins(defaults.getMinMargins()); } - private boolean updateDocument(boolean preview, boolean clearLastError) { + private boolean updateDocument(boolean clearLastError) { if (!clearLastError && mPrintedDocument.hasUpdateError()) { return false; } @@ -902,6 +915,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat mPrintedDocument.clearUpdateError(); } + final boolean preview = mState != STATE_PRINT_CONFIRMED; final PageRange[] pages; if (preview) { pages = mPrintPreviewController.getRequestedPages(); @@ -959,7 +973,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat mPrintPreviewController.closeOptions(); if (canUpdateDocument()) { - updateDocument(false, false); + updateDocument(false); } if (!mPrintedDocument.isUpdating()) { @@ -1433,7 +1447,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat if (mCurrentPrinter.equals(printer)) { setState(STATE_CONFIGURING); if (canUpdateDocument()) { - updateDocument(true, false); + updateDocument(false); } ensurePreviewUiShown(); updateOptionsUi(); @@ -1492,6 +1506,18 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat return true; } + private void shredPagesAndFinish(final Uri writeToUri) { + new PageShredder(this, mPrintJob, mFileProvider, new Runnable() { + @Override + public void run() { + if (writeToUri != null) { + mPrintedDocument.writeContent(getContentResolver(), writeToUri); + } + finish(); + } + }).shred(); + } + private final class SpinnerItem<T> { final T value; final CharSequence label; @@ -1942,7 +1968,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat || (becameActive && hasCapab) || (isActive && gotCapab)); if (updateNeeded && canUpdateDocument()) { - updateDocument(true, false); + updateDocument(false); } updateOptionsUi(); @@ -2033,7 +2059,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat } if (canUpdateDocument()) { - updateDocument(true, false); + updateDocument(false); } updateOptionsUi(); @@ -2158,7 +2184,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat updateOptionsUi(); if (hadErrors && canUpdateDocument()) { - updateDocument(true, false); + updateDocument(false); } } } @@ -2198,4 +2224,176 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat updateOptionsUi(); } } + + private static final class PageShredder implements ServiceConnection { + private static final String TEMP_FILE_PREFIX = "print_job"; + private static final String TEMP_FILE_EXTENSION = ".pdf"; + + private final Context mContext; + + private final MutexFileProvider mFileProvider; + + private final PrintJobInfo mPrintJob; + + private final PageRange[] mPagesToShred; + + private final Runnable mCallback; + + public PageShredder(Context context, PrintJobInfo printJob, + MutexFileProvider fileProvider, Runnable callback) { + mContext = context; + mPrintJob = printJob; + mFileProvider = fileProvider; + mCallback = callback; + mPagesToShred = computePagesToShred(mPrintJob); + } + + public void shred() { + // If we have only the pages we want, done. + if (mPagesToShred.length <= 0) { + mCallback.run(); + return; + } + + // Bind to the manipulation service and the work + // will be performed upon connection to the service. + Intent intent = new Intent(PdfManipulationService.ACTION_GET_EDITOR); + intent.setClass(mContext, PdfManipulationService.class); + mContext.bindService(intent, this, Context.BIND_AUTO_CREATE); + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + final IPdfEditor editor = IPdfEditor.Stub.asInterface(service); + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + try { + // It's OK to access the data members as they are + // final and this code is the last one to touch + // them as shredding is the very last step, so the + // UI is not interactive at this point. + shredPages(editor); + updatePrintJob(); + } finally { + mContext.unbindService(PageShredder.this); + mCallback.run(); + } + return null; + } + }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + /* do nothing */ + } + + private void shredPages(IPdfEditor editor) { + File tempFile = null; + ParcelFileDescriptor src = null; + ParcelFileDescriptor dst = null; + InputStream in = null; + OutputStream out = null; + try { + File jobFile = mFileProvider.acquireFile(null); + src = ParcelFileDescriptor.open(jobFile, ParcelFileDescriptor.MODE_READ_WRITE); + + // Open the document. + editor.openDocument(src); + + // We passed the fd over IPC, close this one. + src.close(); + + // Drop the pages. + editor.removePages(mPagesToShred); + + // Write the modified PDF to a temp file. + tempFile = File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_EXTENSION, + mContext.getCacheDir()); + dst = ParcelFileDescriptor.open(tempFile, ParcelFileDescriptor.MODE_READ_WRITE); + editor.write(dst); + dst.close(); + + // Close the document. + editor.closeDocument(); + + // Copy the temp file over the print job file. + jobFile.delete(); + in = new FileInputStream(tempFile); + out = new FileOutputStream(jobFile); + Streams.copy(in, out); + } catch (IOException|RemoteException e) { + Log.e(LOG_TAG, "Error dropping pages", e); + } finally { + IoUtils.closeQuietly(src); + IoUtils.closeQuietly(dst); + IoUtils.closeQuietly(in); + IoUtils.closeQuietly(out); + if (tempFile != null) { + tempFile.delete(); + } + } + } + + private void updatePrintJob() { + // Update the print job pages. + final int newPageCount = PageRangeUtils.getNormalizedPageCount( + mPrintJob.getPages(), 0); + mPrintJob.setPages(new PageRange[]{PageRange.ALL_PAGES}); + + // Update the print job document info. + PrintDocumentInfo oldDocInfo = mPrintJob.getDocumentInfo(); + PrintDocumentInfo newDocInfo = new PrintDocumentInfo + .Builder(oldDocInfo.getName()) + .setContentType(oldDocInfo.getContentType()) + .setPageCount(newPageCount) + .build(); + mPrintJob.setDocumentInfo(newDocInfo); + } + + private static PageRange[] computePagesToShred(PrintJobInfo printJob) { + List<PageRange> rangesToShred = new ArrayList<>(); + PageRange previousRange = null; + + final int pageCount = printJob.getDocumentInfo().getPageCount(); + + PageRange[] printedPages = printJob.getPages(); + final int rangeCount = printedPages.length; + for (int i = 0; i < rangeCount; i++) { + PageRange range = PageRangeUtils.asAbsoluteRange(printedPages[i], pageCount); + + if (previousRange == null) { + final int startPageIdx = 0; + final int endPageIdx = range.getStart() - 1; + if (startPageIdx <= endPageIdx) { + PageRange removedRange = new PageRange(startPageIdx, endPageIdx); + rangesToShred.add(removedRange); + } + } else { + final int startPageIdx = previousRange.getEnd() + 1; + final int endPageIdx = range.getStart() - 1; + if (startPageIdx <= endPageIdx) { + PageRange removedRange = new PageRange(startPageIdx, endPageIdx); + rangesToShred.add(removedRange); + } + } + + if (i == rangeCount - 1) { + final int startPageIdx = range.getEnd() + 1; + final int endPageIdx = printJob.getDocumentInfo().getPageCount() - 1; + if (startPageIdx <= endPageIdx) { + PageRange removedRange = new PageRange(startPageIdx, endPageIdx); + rangesToShred.add(removedRange); + } + } + + previousRange = range; + } + + PageRange[] result = new PageRange[rangesToShred.size()]; + rangesToShred.toArray(result); + return result; + } + } } diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintPreviewController.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintPreviewController.java index a25e05e..5d858ca 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintPreviewController.java +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintPreviewController.java @@ -185,8 +185,10 @@ class PrintPreviewController implements MutexFileProvider.OnReleaseRequestCallba public void run() { // At this point the other end will write to the file, hence // we have to close it and reopen after the write completes. - Message operation = mHandler.obtainMessage(MyHandler.MSG_CLOSE); - mHandler.enqueueOperation(operation); + if (mPageAdapter.isOpened()) { + Message operation = mHandler.obtainMessage(MyHandler.MSG_CLOSE); + mHandler.enqueueOperation(operation); + } } }); } |