| /* |
| * 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.model; |
| |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder.DeathRecipient; |
| import android.os.ICancellationSignal; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.ParcelFileDescriptor; |
| import android.os.RemoteException; |
| import android.print.ILayoutResultCallback; |
| import android.print.IPrintDocumentAdapter; |
| import android.print.IPrintDocumentAdapterObserver; |
| import android.print.IWriteResultCallback; |
| import android.print.PageRange; |
| import android.print.PrintAttributes; |
| import android.print.PrintDocumentAdapter; |
| import android.print.PrintDocumentInfo; |
| import android.util.Log; |
| |
| import com.android.printspooler.R; |
| import com.android.printspooler.util.PageRangeUtils; |
| |
| import libcore.io.IoUtils; |
| |
| 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.lang.ref.WeakReference; |
| import java.util.Arrays; |
| |
| public final class RemotePrintDocument { |
| private static final String LOG_TAG = "RemotePrintDocument"; |
| |
| private static final boolean DEBUG = false; |
| |
| private static final int STATE_INITIAL = 0; |
| private static final int STATE_STARTED = 1; |
| private static final int STATE_UPDATING = 2; |
| private static final int STATE_UPDATED = 3; |
| private static final int STATE_FAILED = 4; |
| private static final int STATE_FINISHED = 5; |
| private static final int STATE_CANCELING = 6; |
| private static final int STATE_CANCELED = 7; |
| private static final int STATE_DESTROYED = 8; |
| |
| private final Context mContext; |
| |
| private final RemotePrintDocumentInfo mDocumentInfo; |
| private final UpdateSpec mUpdateSpec = new UpdateSpec(); |
| |
| private final Looper mLooper; |
| private final IPrintDocumentAdapter mPrintDocumentAdapter; |
| private final RemoteAdapterDeathObserver mAdapterDeathObserver; |
| |
| private final UpdateResultCallbacks mUpdateCallbacks; |
| |
| private final CommandDoneCallback mCommandResultCallback = |
| new CommandDoneCallback() { |
| @Override |
| public void onDone() { |
| if (mCurrentCommand.isCompleted()) { |
| if (mCurrentCommand instanceof LayoutCommand) { |
| // If there is a next command after a layout is done, then another |
| // update was issued and the next command is another layout, so we |
| // do nothing. However, if there is no next command we may need to |
| // ask for some pages given we do not already have them or we do |
| // but the content has changed. |
| if (mNextCommand == null) { |
| if (mUpdateSpec.pages != null && (mDocumentInfo.changed |
| || (mDocumentInfo.info.getPageCount() |
| != PrintDocumentInfo.PAGE_COUNT_UNKNOWN |
| && !PageRangeUtils.contains(mDocumentInfo.writtenPages, |
| mUpdateSpec.pages, mDocumentInfo.info.getPageCount())))) { |
| mNextCommand = new WriteCommand(mContext, mLooper, |
| mPrintDocumentAdapter, mDocumentInfo, |
| mDocumentInfo.info.getPageCount(), mUpdateSpec.pages, |
| mDocumentInfo.fileProvider, mCommandResultCallback); |
| } else { |
| if (mUpdateSpec.pages != null) { |
| // If we have the requested pages, update which ones to be printed. |
| mDocumentInfo.printedPages = PageRangeUtils.computePrintedPages( |
| mUpdateSpec.pages, mDocumentInfo.writtenPages, |
| mDocumentInfo.info.getPageCount()); |
| } |
| // Notify we are done. |
| mState = STATE_UPDATED; |
| notifyUpdateCompleted(); |
| } |
| } |
| } else { |
| // We always notify after a write. |
| mState = STATE_UPDATED; |
| notifyUpdateCompleted(); |
| } |
| runPendingCommand(); |
| } else if (mCurrentCommand.isFailed()) { |
| mState = STATE_FAILED; |
| CharSequence error = mCurrentCommand.getError(); |
| mCurrentCommand = null; |
| mNextCommand = null; |
| mUpdateSpec.reset(); |
| notifyUpdateFailed(error); |
| } else if (mCurrentCommand.isCanceled()) { |
| if (mState == STATE_CANCELING) { |
| mState = STATE_CANCELED; |
| notifyUpdateCanceled(); |
| } |
| runPendingCommand(); |
| } |
| } |
| }; |
| |
| private final DeathRecipient mDeathRecipient = new DeathRecipient() { |
| @Override |
| public void binderDied() { |
| notifyPrintingAppDied(); |
| } |
| }; |
| |
| private int mState = STATE_INITIAL; |
| |
| private AsyncCommand mCurrentCommand; |
| private AsyncCommand mNextCommand; |
| |
| public interface RemoteAdapterDeathObserver { |
| public void onDied(); |
| } |
| |
| public interface UpdateResultCallbacks { |
| public void onUpdateCompleted(RemotePrintDocumentInfo document); |
| public void onUpdateCanceled(); |
| public void onUpdateFailed(CharSequence error); |
| } |
| |
| public RemotePrintDocument(Context context, IPrintDocumentAdapter adapter, |
| MutexFileProvider fileProvider, RemoteAdapterDeathObserver deathObserver, |
| UpdateResultCallbacks callbacks) { |
| mPrintDocumentAdapter = adapter; |
| mLooper = context.getMainLooper(); |
| mContext = context; |
| mAdapterDeathObserver = deathObserver; |
| mDocumentInfo = new RemotePrintDocumentInfo(); |
| mDocumentInfo.fileProvider = fileProvider; |
| mUpdateCallbacks = callbacks; |
| connectToRemoteDocument(); |
| } |
| |
| public void start() { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[CALLED] start()"); |
| } |
| if (mState != STATE_INITIAL) { |
| throw new IllegalStateException("Cannot start in state:" + stateToString(mState)); |
| } |
| try { |
| mPrintDocumentAdapter.start(); |
| mState = STATE_STARTED; |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error calling start()", re); |
| mState = STATE_FAILED; |
| } |
| } |
| |
| public boolean update(PrintAttributes attributes, PageRange[] pages, boolean preview) { |
| boolean willUpdate; |
| |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[CALLED] update()"); |
| } |
| |
| if (hasUpdateError()) { |
| throw new IllegalStateException("Cannot update without a clearing the failure"); |
| } |
| |
| if (mState == STATE_INITIAL || mState == STATE_FINISHED || mState == STATE_DESTROYED) { |
| throw new IllegalStateException("Cannot update in state:" + stateToString(mState)); |
| } |
| |
| // We schedule a layout if the constraints changed. |
| if (!mUpdateSpec.hasSameConstraints(attributes, preview)) { |
| willUpdate = true; |
| |
| // If there is a current command that is running we ask for a |
| // cancellation and start over. |
| if (mCurrentCommand != null && (mCurrentCommand.isRunning() |
| || mCurrentCommand.isPending())) { |
| mCurrentCommand.cancel(); |
| } |
| |
| // Schedule a layout command. |
| PrintAttributes oldAttributes = mDocumentInfo.attributes != null |
| ? mDocumentInfo.attributes : new PrintAttributes.Builder().build(); |
| AsyncCommand command = new LayoutCommand(mLooper, mPrintDocumentAdapter, |
| mDocumentInfo, oldAttributes, attributes, preview, mCommandResultCallback); |
| scheduleCommand(command); |
| |
| mState = STATE_UPDATING; |
| // If no layout in progress and we don't have all pages - schedule a write. |
| } else if ((!(mCurrentCommand instanceof LayoutCommand) |
| || (!mCurrentCommand.isPending() && !mCurrentCommand.isRunning())) |
| && pages != null && !PageRangeUtils.contains(mUpdateSpec.pages, pages, |
| mDocumentInfo.info.getPageCount())) { |
| willUpdate = true; |
| |
| // Cancel the current write as a new one is to be scheduled. |
| if (mCurrentCommand instanceof WriteCommand |
| && (mCurrentCommand.isPending() || mCurrentCommand.isRunning())) { |
| mCurrentCommand.cancel(); |
| } |
| |
| // Schedule a write command. |
| AsyncCommand command = new WriteCommand(mContext, mLooper, mPrintDocumentAdapter, |
| mDocumentInfo, mDocumentInfo.info.getPageCount(), pages, |
| mDocumentInfo.fileProvider, mCommandResultCallback); |
| scheduleCommand(command); |
| |
| mState = STATE_UPDATING; |
| } else { |
| willUpdate = false; |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[SKIPPING] No update needed"); |
| } |
| } |
| |
| // Keep track of what is requested. |
| mUpdateSpec.update(attributes, preview, pages); |
| |
| runPendingCommand(); |
| |
| return willUpdate; |
| } |
| |
| public void finish() { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[CALLED] finish()"); |
| } |
| if (mState != STATE_STARTED && mState != STATE_UPDATED |
| && mState != STATE_FAILED && mState != STATE_CANCELING |
| && mState != STATE_CANCELED) { |
| throw new IllegalStateException("Cannot finish in state:" |
| + stateToString(mState)); |
| } |
| try { |
| mPrintDocumentAdapter.finish(); |
| mState = STATE_FINISHED; |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error calling finish()", re); |
| mState = STATE_FAILED; |
| } |
| } |
| |
| public void cancel() { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[CALLED] cancel()"); |
| } |
| |
| if (mState == STATE_CANCELING) { |
| return; |
| } |
| |
| if (mState != STATE_UPDATING) { |
| throw new IllegalStateException("Cannot cancel in state:" + stateToString(mState)); |
| } |
| |
| mState = STATE_CANCELING; |
| |
| mCurrentCommand.cancel(); |
| } |
| |
| public void destroy() { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[CALLED] destroy()"); |
| } |
| if (mState == STATE_DESTROYED) { |
| throw new IllegalStateException("Cannot destroy in state:" + stateToString(mState)); |
| } |
| |
| mState = STATE_DESTROYED; |
| |
| disconnectFromRemoteDocument(); |
| } |
| |
| public boolean isUpdating() { |
| return mState == STATE_UPDATING || mState == STATE_CANCELING; |
| } |
| |
| public boolean isDestroyed() { |
| return mState == STATE_DESTROYED; |
| } |
| |
| public boolean hasUpdateError() { |
| return mState == STATE_FAILED; |
| } |
| |
| public boolean hasLaidOutPages() { |
| return mDocumentInfo.info != null |
| && mDocumentInfo.info.getPageCount() > 0; |
| } |
| |
| public void clearUpdateError() { |
| if (!hasUpdateError()) { |
| throw new IllegalStateException("No update error to clear"); |
| } |
| mState = STATE_STARTED; |
| } |
| |
| public RemotePrintDocumentInfo getDocumentInfo() { |
| return mDocumentInfo; |
| } |
| |
| public void writeContent(ContentResolver contentResolver, Uri uri) { |
| File file = null; |
| InputStream in = null; |
| OutputStream out = null; |
| try { |
| file = mDocumentInfo.fileProvider.acquireFile(null); |
| in = new FileInputStream(file); |
| out = contentResolver.openOutputStream(uri); |
| final byte[] buffer = new byte[8192]; |
| while (true) { |
| final int readByteCount = in.read(buffer); |
| if (readByteCount < 0) { |
| break; |
| } |
| out.write(buffer, 0, readByteCount); |
| } |
| } catch (IOException e) { |
| Log.e(LOG_TAG, "Error writing document content.", e); |
| } finally { |
| IoUtils.closeQuietly(in); |
| IoUtils.closeQuietly(out); |
| if (file != null) { |
| mDocumentInfo.fileProvider.releaseFile(); |
| } |
| } |
| } |
| |
| private void notifyUpdateCanceled() { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[CALLING] onUpdateCanceled()"); |
| } |
| mUpdateCallbacks.onUpdateCanceled(); |
| } |
| |
| private void notifyUpdateCompleted() { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[CALLING] onUpdateCompleted()"); |
| } |
| mUpdateCallbacks.onUpdateCompleted(mDocumentInfo); |
| } |
| |
| private void notifyUpdateFailed(CharSequence error) { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[CALLING] onUpdateCompleted()"); |
| } |
| mUpdateCallbacks.onUpdateFailed(error); |
| } |
| |
| private void connectToRemoteDocument() { |
| try { |
| mPrintDocumentAdapter.asBinder().linkToDeath(mDeathRecipient, 0); |
| } catch (RemoteException re) { |
| Log.w(LOG_TAG, "The printing process is dead."); |
| destroy(); |
| return; |
| } |
| |
| try { |
| mPrintDocumentAdapter.setObserver(new PrintDocumentAdapterObserver(this)); |
| } catch (RemoteException re) { |
| Log.w(LOG_TAG, "Error setting observer to the print adapter."); |
| destroy(); |
| } |
| } |
| |
| private void disconnectFromRemoteDocument() { |
| try { |
| mPrintDocumentAdapter.setObserver(null); |
| } catch (RemoteException re) { |
| Log.w(LOG_TAG, "Error setting observer to the print adapter."); |
| // Keep going - best effort... |
| } |
| |
| mPrintDocumentAdapter.asBinder().unlinkToDeath(mDeathRecipient, 0); |
| } |
| |
| private void scheduleCommand(AsyncCommand command) { |
| if (mCurrentCommand == null) { |
| mCurrentCommand = command; |
| } else { |
| mNextCommand = command; |
| } |
| } |
| |
| private void runPendingCommand() { |
| if (mCurrentCommand != null |
| && (mCurrentCommand.isCompleted() |
| || mCurrentCommand.isCanceled())) { |
| mCurrentCommand = mNextCommand; |
| mNextCommand = null; |
| } |
| |
| if (mCurrentCommand != null) { |
| if (mCurrentCommand.isPending()) { |
| mCurrentCommand.run(); |
| } |
| mState = STATE_UPDATING; |
| } else { |
| mState = STATE_UPDATED; |
| } |
| } |
| |
| private static String stateToString(int state) { |
| switch (state) { |
| case STATE_FINISHED: { |
| return "STATE_FINISHED"; |
| } |
| case STATE_FAILED: { |
| return "STATE_FAILED"; |
| } |
| case STATE_STARTED: { |
| return "STATE_STARTED"; |
| } |
| case STATE_UPDATING: { |
| return "STATE_UPDATING"; |
| } |
| case STATE_UPDATED: { |
| return "STATE_UPDATED"; |
| } |
| case STATE_CANCELING: { |
| return "STATE_CANCELING"; |
| } |
| case STATE_CANCELED: { |
| return "STATE_CANCELED"; |
| } |
| case STATE_DESTROYED: { |
| return "STATE_DESTROYED"; |
| } |
| default: { |
| return "STATE_UNKNOWN"; |
| } |
| } |
| } |
| |
| static final class UpdateSpec { |
| final PrintAttributes attributes = new PrintAttributes.Builder().build(); |
| boolean preview; |
| PageRange[] pages; |
| |
| public void update(PrintAttributes attributes, boolean preview, |
| PageRange[] pages) { |
| this.attributes.copyFrom(attributes); |
| this.preview = preview; |
| this.pages = (pages != null) ? Arrays.copyOf(pages, pages.length) : null; |
| } |
| |
| public void reset() { |
| attributes.clear(); |
| preview = false; |
| pages = null; |
| } |
| |
| public boolean hasSameConstraints(PrintAttributes attributes, boolean preview) { |
| return this.attributes.equals(attributes) && this.preview == preview; |
| } |
| } |
| |
| public static final class RemotePrintDocumentInfo { |
| public PrintAttributes attributes; |
| public Bundle metadata; |
| public PrintDocumentInfo info; |
| public PageRange[] printedPages; |
| public PageRange[] writtenPages; |
| public MutexFileProvider fileProvider; |
| public boolean changed; |
| public boolean updated; |
| public boolean laidout; |
| } |
| |
| private interface CommandDoneCallback { |
| public void onDone(); |
| } |
| |
| private static abstract class AsyncCommand implements Runnable { |
| private static final int STATE_PENDING = 0; |
| private static final int STATE_RUNNING = 1; |
| private static final int STATE_COMPLETED = 2; |
| private static final int STATE_CANCELED = 3; |
| private static final int STATE_CANCELING = 4; |
| private static final int STATE_FAILED = 5; |
| |
| private static int sSequenceCounter; |
| |
| protected final int mSequence = sSequenceCounter++; |
| protected final IPrintDocumentAdapter mAdapter; |
| protected final RemotePrintDocumentInfo mDocument; |
| |
| protected final CommandDoneCallback mDoneCallback; |
| |
| protected ICancellationSignal mCancellation; |
| |
| private CharSequence mError; |
| |
| private int mState = STATE_PENDING; |
| |
| public AsyncCommand(IPrintDocumentAdapter adapter, RemotePrintDocumentInfo document, |
| CommandDoneCallback doneCallback) { |
| mAdapter = adapter; |
| mDocument = document; |
| mDoneCallback = doneCallback; |
| } |
| |
| protected final boolean isCanceling() { |
| return mState == STATE_CANCELING; |
| } |
| |
| public final boolean isCanceled() { |
| return mState == STATE_CANCELED; |
| } |
| |
| public final void cancel() { |
| if (isRunning()) { |
| canceling(); |
| if (mCancellation != null) { |
| try { |
| mCancellation.cancel(); |
| } catch (RemoteException re) { |
| Log.w(LOG_TAG, "Error while canceling", re); |
| } |
| } |
| } else { |
| canceled(); |
| |
| // Done. |
| mDoneCallback.onDone(); |
| } |
| } |
| |
| protected final void canceling() { |
| if (mState != STATE_PENDING && mState != STATE_RUNNING) { |
| throw new IllegalStateException("Command not pending or running."); |
| } |
| mState = STATE_CANCELING; |
| } |
| |
| protected final void canceled() { |
| if (mState != STATE_CANCELING) { |
| throw new IllegalStateException("Not canceling."); |
| } |
| mState = STATE_CANCELED; |
| } |
| |
| public final boolean isPending() { |
| return mState == STATE_PENDING; |
| } |
| |
| protected final void running() { |
| if (mState != STATE_PENDING) { |
| throw new IllegalStateException("Not pending."); |
| } |
| mState = STATE_RUNNING; |
| } |
| |
| public final boolean isRunning() { |
| return mState == STATE_RUNNING; |
| } |
| |
| protected final void completed() { |
| if (mState != STATE_RUNNING && mState != STATE_CANCELING) { |
| throw new IllegalStateException("Not running."); |
| } |
| mState = STATE_COMPLETED; |
| } |
| |
| public final boolean isCompleted() { |
| return mState == STATE_COMPLETED; |
| } |
| |
| protected final void failed(CharSequence error) { |
| if (mState != STATE_RUNNING) { |
| throw new IllegalStateException("Not running."); |
| } |
| mState = STATE_FAILED; |
| |
| mError = error; |
| } |
| |
| public final boolean isFailed() { |
| return mState == STATE_FAILED; |
| } |
| |
| public CharSequence getError() { |
| return mError; |
| } |
| } |
| |
| private static final class LayoutCommand extends AsyncCommand { |
| private final PrintAttributes mOldAttributes = new PrintAttributes.Builder().build(); |
| private final PrintAttributes mNewAttributes = new PrintAttributes.Builder().build(); |
| private final Bundle mMetadata = new Bundle(); |
| |
| private final ILayoutResultCallback mRemoteResultCallback; |
| |
| private final Handler mHandler; |
| |
| public LayoutCommand(Looper looper, IPrintDocumentAdapter adapter, |
| RemotePrintDocumentInfo document, PrintAttributes oldAttributes, |
| PrintAttributes newAttributes, boolean preview, CommandDoneCallback callback) { |
| super(adapter, document, callback); |
| mHandler = new LayoutHandler(looper); |
| mRemoteResultCallback = new LayoutResultCallback(mHandler); |
| mOldAttributes.copyFrom(oldAttributes); |
| mNewAttributes.copyFrom(newAttributes); |
| mMetadata.putBoolean(PrintDocumentAdapter.EXTRA_PRINT_PREVIEW, preview); |
| } |
| |
| @Override |
| public void run() { |
| running(); |
| |
| try { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[PERFORMING] layout"); |
| } |
| mDocument.changed = false; |
| mAdapter.layout(mOldAttributes, mNewAttributes, mRemoteResultCallback, |
| mMetadata, mSequence); |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error calling layout", re); |
| handleOnLayoutFailed(null, mSequence); |
| } |
| } |
| |
| private void handleOnLayoutStarted(ICancellationSignal cancellation, int sequence) { |
| if (sequence != mSequence) { |
| return; |
| } |
| |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[CALLBACK] onLayoutStarted"); |
| } |
| |
| if (isCanceling()) { |
| try { |
| cancellation.cancel(); |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error cancelling", re); |
| handleOnLayoutFailed(null, mSequence); |
| } |
| } else { |
| mCancellation = cancellation; |
| } |
| } |
| |
| private void handleOnLayoutFinished(PrintDocumentInfo info, |
| boolean changed, int sequence) { |
| if (sequence != mSequence) { |
| return; |
| } |
| |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[CALLBACK] onLayoutFinished"); |
| } |
| |
| completed(); |
| |
| // If the document description changed or the content in the |
| // document changed, the we need to invalidate the pages. |
| if (changed || !equalsIgnoreSize(mDocument.info, info)) { |
| // If the content changed we throw away all pages as |
| // we will request them again with the new content. |
| mDocument.writtenPages = null; |
| mDocument.printedPages = null; |
| mDocument.changed = true; |
| } |
| |
| // Update the document with data from the layout pass. |
| mDocument.attributes = mNewAttributes; |
| mDocument.metadata = mMetadata; |
| mDocument.laidout = true; |
| mDocument.info = info; |
| |
| // Release the remote cancellation interface. |
| mCancellation = null; |
| |
| // Done. |
| mDoneCallback.onDone(); |
| } |
| |
| private void handleOnLayoutFailed(CharSequence error, int sequence) { |
| if (sequence != mSequence) { |
| return; |
| } |
| |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[CALLBACK] onLayoutFailed"); |
| } |
| |
| mDocument.laidout = false; |
| |
| failed(error); |
| |
| // Release the remote cancellation interface. |
| mCancellation = null; |
| |
| // Failed. |
| mDoneCallback.onDone(); |
| } |
| |
| private void handleOnLayoutCanceled(int sequence) { |
| if (sequence != mSequence) { |
| return; |
| } |
| |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[CALLBACK] onLayoutCanceled"); |
| } |
| |
| canceled(); |
| |
| // Release the remote cancellation interface. |
| mCancellation = null; |
| |
| // Done. |
| mDoneCallback.onDone(); |
| } |
| |
| private boolean equalsIgnoreSize(PrintDocumentInfo lhs, PrintDocumentInfo rhs) { |
| if (lhs == rhs) { |
| return true; |
| } |
| if (lhs == null) { |
| return false; |
| } else { |
| if (rhs == null) { |
| return false; |
| } |
| if (lhs.getContentType() != rhs.getContentType() |
| || lhs.getPageCount() != rhs.getPageCount()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private final class LayoutHandler extends Handler { |
| public static final int MSG_ON_LAYOUT_STARTED = 1; |
| public static final int MSG_ON_LAYOUT_FINISHED = 2; |
| public static final int MSG_ON_LAYOUT_FAILED = 3; |
| public static final int MSG_ON_LAYOUT_CANCELED = 4; |
| |
| public LayoutHandler(Looper looper) { |
| super(looper, null, false); |
| } |
| |
| @Override |
| public void handleMessage(Message message) { |
| switch (message.what) { |
| case MSG_ON_LAYOUT_STARTED: { |
| ICancellationSignal cancellation = (ICancellationSignal) message.obj; |
| final int sequence = message.arg1; |
| handleOnLayoutStarted(cancellation, sequence); |
| } break; |
| |
| case MSG_ON_LAYOUT_FINISHED: { |
| PrintDocumentInfo info = (PrintDocumentInfo) message.obj; |
| final boolean changed = (message.arg1 == 1); |
| final int sequence = message.arg2; |
| handleOnLayoutFinished(info, changed, sequence); |
| } break; |
| |
| case MSG_ON_LAYOUT_FAILED: { |
| CharSequence error = (CharSequence) message.obj; |
| final int sequence = message.arg1; |
| handleOnLayoutFailed(error, sequence); |
| } break; |
| |
| case MSG_ON_LAYOUT_CANCELED: { |
| final int sequence = message.arg1; |
| handleOnLayoutCanceled(sequence); |
| } break; |
| } |
| } |
| } |
| |
| private static final class LayoutResultCallback extends ILayoutResultCallback.Stub { |
| private final WeakReference<Handler> mWeakHandler; |
| |
| public LayoutResultCallback(Handler handler) { |
| mWeakHandler = new WeakReference<>(handler); |
| } |
| |
| @Override |
| public void onLayoutStarted(ICancellationSignal cancellation, int sequence) { |
| Handler handler = mWeakHandler.get(); |
| if (handler != null) { |
| handler.obtainMessage(LayoutHandler.MSG_ON_LAYOUT_STARTED, |
| sequence, 0, cancellation).sendToTarget(); |
| } |
| } |
| |
| @Override |
| public void onLayoutFinished(PrintDocumentInfo info, boolean changed, int sequence) { |
| Handler handler = mWeakHandler.get(); |
| if (handler != null) { |
| handler.obtainMessage(LayoutHandler.MSG_ON_LAYOUT_FINISHED, |
| changed ? 1 : 0, sequence, info).sendToTarget(); |
| } |
| } |
| |
| @Override |
| public void onLayoutFailed(CharSequence error, int sequence) { |
| Handler handler = mWeakHandler.get(); |
| if (handler != null) { |
| handler.obtainMessage(LayoutHandler.MSG_ON_LAYOUT_FAILED, |
| sequence, 0, error).sendToTarget(); |
| } |
| } |
| |
| @Override |
| public void onLayoutCanceled(int sequence) { |
| Handler handler = mWeakHandler.get(); |
| if (handler != null) { |
| handler.obtainMessage(LayoutHandler.MSG_ON_LAYOUT_CANCELED, |
| sequence, 0).sendToTarget(); |
| } |
| } |
| } |
| } |
| |
| private static final class WriteCommand extends AsyncCommand { |
| private final int mPageCount; |
| private final PageRange[] mPages; |
| private final MutexFileProvider mFileProvider; |
| |
| private final IWriteResultCallback mRemoteResultCallback; |
| private final CommandDoneCallback mDoneCallback; |
| |
| private final Context mContext; |
| private final Handler mHandler; |
| |
| public WriteCommand(Context context, Looper looper, IPrintDocumentAdapter adapter, |
| RemotePrintDocumentInfo document, int pageCount, PageRange[] pages, |
| MutexFileProvider fileProvider, CommandDoneCallback callback) { |
| super(adapter, document, callback); |
| mContext = context; |
| mHandler = new WriteHandler(looper); |
| mRemoteResultCallback = new WriteResultCallback(mHandler); |
| mPageCount = pageCount; |
| mPages = Arrays.copyOf(pages, pages.length); |
| mFileProvider = fileProvider; |
| mDoneCallback = callback; |
| } |
| |
| @Override |
| public void run() { |
| running(); |
| |
| // This is a long running operation as we will be reading fully |
| // the written data. In case of a cancellation, we ask the client |
| // to stop writing data and close the file descriptor after |
| // which we will reach the end of the stream, thus stop reading. |
| new AsyncTask<Void, Void, Void>() { |
| @Override |
| protected Void doInBackground(Void... params) { |
| File file = null; |
| InputStream in = null; |
| OutputStream out = null; |
| ParcelFileDescriptor source = null; |
| ParcelFileDescriptor sink = null; |
| try { |
| file = mFileProvider.acquireFile(null); |
| ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); |
| source = pipe[0]; |
| sink = pipe[1]; |
| |
| in = new FileInputStream(source.getFileDescriptor()); |
| out = new FileOutputStream(file); |
| |
| // Async call to initiate the other process writing the data. |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[PERFORMING] write"); |
| } |
| mAdapter.write(mPages, sink, mRemoteResultCallback, mSequence); |
| |
| // Close the source. It is now held by the client. |
| sink.close(); |
| sink = null; |
| |
| // Read the data. |
| final byte[] buffer = new byte[8192]; |
| while (true) { |
| final int readByteCount = in.read(buffer); |
| if (readByteCount < 0) { |
| break; |
| } |
| out.write(buffer, 0, readByteCount); |
| } |
| } catch (RemoteException | IOException e) { |
| Log.e(LOG_TAG, "Error calling write()", e); |
| } finally { |
| IoUtils.closeQuietly(in); |
| IoUtils.closeQuietly(out); |
| IoUtils.closeQuietly(sink); |
| IoUtils.closeQuietly(source); |
| if (file != null) { |
| mFileProvider.releaseFile(); |
| } |
| } |
| return null; |
| } |
| }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); |
| } |
| |
| private void handleOnWriteStarted(ICancellationSignal cancellation, int sequence) { |
| if (sequence != mSequence) { |
| return; |
| } |
| |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[CALLBACK] onWriteStarted"); |
| } |
| |
| if (isCanceling()) { |
| try { |
| cancellation.cancel(); |
| } catch (RemoteException re) { |
| Log.e(LOG_TAG, "Error cancelling", re); |
| handleOnWriteFailed(null, sequence); |
| } |
| } else { |
| mCancellation = cancellation; |
| } |
| } |
| |
| private void handleOnWriteFinished(PageRange[] pages, int sequence) { |
| if (sequence != mSequence) { |
| return; |
| } |
| |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[CALLBACK] onWriteFinished"); |
| } |
| |
| PageRange[] writtenPages = PageRangeUtils.normalize(pages); |
| PageRange[] printedPages = PageRangeUtils.computePrintedPages( |
| mPages, writtenPages, mPageCount); |
| |
| // Handle if we got invalid pages |
| if (printedPages != null) { |
| mDocument.writtenPages = writtenPages; |
| mDocument.printedPages = printedPages; |
| completed(); |
| } else { |
| mDocument.writtenPages = null; |
| mDocument.printedPages = null; |
| failed(mContext.getString(R.string.print_error_default_message)); |
| } |
| |
| // Release the remote cancellation interface. |
| mCancellation = null; |
| |
| // Done. |
| mDoneCallback.onDone(); |
| } |
| |
| private void handleOnWriteFailed(CharSequence error, int sequence) { |
| if (sequence != mSequence) { |
| return; |
| } |
| |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[CALLBACK] onWriteFailed"); |
| } |
| |
| failed(error); |
| |
| // Release the remote cancellation interface. |
| mCancellation = null; |
| |
| // Done. |
| mDoneCallback.onDone(); |
| } |
| |
| private void handleOnWriteCanceled(int sequence) { |
| if (sequence != mSequence) { |
| return; |
| } |
| |
| if (DEBUG) { |
| Log.i(LOG_TAG, "[CALLBACK] onWriteCanceled"); |
| } |
| |
| canceled(); |
| |
| // Release the remote cancellation interface. |
| mCancellation = null; |
| |
| // Done. |
| mDoneCallback.onDone(); |
| } |
| |
| private final class WriteHandler extends Handler { |
| public static final int MSG_ON_WRITE_STARTED = 1; |
| public static final int MSG_ON_WRITE_FINISHED = 2; |
| public static final int MSG_ON_WRITE_FAILED = 3; |
| public static final int MSG_ON_WRITE_CANCELED = 4; |
| |
| public WriteHandler(Looper looper) { |
| super(looper, null, false); |
| } |
| |
| @Override |
| public void handleMessage(Message message) { |
| switch (message.what) { |
| case MSG_ON_WRITE_STARTED: { |
| ICancellationSignal cancellation = (ICancellationSignal) message.obj; |
| final int sequence = message.arg1; |
| handleOnWriteStarted(cancellation, sequence); |
| } break; |
| |
| case MSG_ON_WRITE_FINISHED: { |
| PageRange[] pages = (PageRange[]) message.obj; |
| final int sequence = message.arg1; |
| handleOnWriteFinished(pages, sequence); |
| } break; |
| |
| case MSG_ON_WRITE_FAILED: { |
| CharSequence error = (CharSequence) message.obj; |
| final int sequence = message.arg1; |
| handleOnWriteFailed(error, sequence); |
| } break; |
| |
| case MSG_ON_WRITE_CANCELED: { |
| final int sequence = message.arg1; |
| handleOnWriteCanceled(sequence); |
| } break; |
| } |
| } |
| } |
| |
| private static final class WriteResultCallback extends IWriteResultCallback.Stub { |
| private final WeakReference<Handler> mWeakHandler; |
| |
| public WriteResultCallback(Handler handler) { |
| mWeakHandler = new WeakReference<>(handler); |
| } |
| |
| @Override |
| public void onWriteStarted(ICancellationSignal cancellation, int sequence) { |
| Handler handler = mWeakHandler.get(); |
| if (handler != null) { |
| handler.obtainMessage(WriteHandler.MSG_ON_WRITE_STARTED, |
| sequence, 0, cancellation).sendToTarget(); |
| } |
| } |
| |
| @Override |
| public void onWriteFinished(PageRange[] pages, int sequence) { |
| Handler handler = mWeakHandler.get(); |
| if (handler != null) { |
| handler.obtainMessage(WriteHandler.MSG_ON_WRITE_FINISHED, |
| sequence, 0, pages).sendToTarget(); |
| } |
| } |
| |
| @Override |
| public void onWriteFailed(CharSequence error, int sequence) { |
| Handler handler = mWeakHandler.get(); |
| if (handler != null) { |
| handler.obtainMessage(WriteHandler.MSG_ON_WRITE_FAILED, |
| sequence, 0, error).sendToTarget(); |
| } |
| } |
| |
| @Override |
| public void onWriteCanceled(int sequence) { |
| Handler handler = mWeakHandler.get(); |
| if (handler != null) { |
| handler.obtainMessage(WriteHandler.MSG_ON_WRITE_CANCELED, |
| sequence, 0).sendToTarget(); |
| } |
| } |
| } |
| } |
| |
| private void notifyPrintingAppDied() { |
| new Handler(mLooper).post(new Runnable() { |
| @Override |
| public void run() { |
| mAdapterDeathObserver.onDied(); |
| } |
| }); |
| } |
| |
| private static final class PrintDocumentAdapterObserver |
| extends IPrintDocumentAdapterObserver.Stub { |
| private final WeakReference<RemotePrintDocument> mWeakDocument; |
| |
| public PrintDocumentAdapterObserver(RemotePrintDocument document) { |
| mWeakDocument = new WeakReference<>(document); |
| } |
| |
| @Override |
| public void onDestroy() { |
| final RemotePrintDocument document = mWeakDocument.get(); |
| if (document != null) { |
| document.notifyPrintingAppDied(); |
| } |
| } |
| } |
| } |