diff options
Diffstat (limited to 'packages/PrintSpooler/src')
4 files changed, 1934 insertions, 0 deletions
diff --git a/packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java b/packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java new file mode 100644 index 0000000..ae2fe5c --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java @@ -0,0 +1,794 @@ +/* + * Copyright (C) 2013 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; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.os.IBinder.DeathRecipient; +import android.print.IPrintAdapter; +import android.print.IPrintManager; +import android.print.IPrinterDiscoveryObserver; +import android.print.PageRange; +import android.print.PrintAttributes; +import android.print.PrintAttributes.MediaSize; +import android.print.PrintAttributes.Resolution; +import android.print.PrintAttributes.Tray; +import android.print.PrintJobInfo; +import android.print.PrinterId; +import android.print.PrinterInfo; +import android.text.Editable; +import android.text.InputFilter; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.Spinner; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Activity for configuring a print job. + */ +public class PrintJobConfigActivity extends Activity { + + private static final boolean DEBUG = false; + + private static final String LOG_TAG = PrintJobConfigActivity.class.getSimpleName(); + + public static final String EXTRA_PRINTABLE = "printable"; + public static final String EXTRA_APP_ID = "appId"; + public static final String EXTRA_ATTRIBUTES = "attributes"; + public static final String EXTRA_PRINT_JOB_ID = "printJobId"; + + private static final int MIN_COPIES = 1; + + private final List<QueuedAsyncTask<?>> mTaskQueue = new ArrayList<QueuedAsyncTask<?>>(); + + private IPrintManager mPrintManager; + + private IPrinterDiscoveryObserver mPrinterDiscoveryObserver; + + private int mAppId; + private int mPrintJobId; + + private PrintAttributes mPrintAttributes; + + private final PrintSpooler mPrintSpooler = PrintSpooler.getInstance(this); + + private RemotePrintAdapter mRemotePrintAdapter; + + // UI elements + + private EditText mCopiesEditText; + + private Spinner mDestinationSpinner; + public ArrayAdapter<SpinnerItem<PrinterInfo>> mDestinationSpinnerAdapter; + + private Spinner mMediaSizeSpinner; + public ArrayAdapter<SpinnerItem<MediaSize>> mMediaSizeSpinnerAdapter; + + private Spinner mResolutionSpinner; + public ArrayAdapter<SpinnerItem<Resolution>> mResolutionSpinnerAdapter; + + private Spinner mInputTraySpinner; + public ArrayAdapter<SpinnerItem<Tray>> mInputTraySpinnerAdapter; + + private Spinner mOutputTraySpinner; + public ArrayAdapter<SpinnerItem<Tray>> mOutputTraySpinnerAdapter; + + private Spinner mDuplexModeSpinner; + public ArrayAdapter<SpinnerItem<Integer>> mDuplexModeSpinnerAdapter; + + private Spinner mColorModeSpinner; + public ArrayAdapter<SpinnerItem<Integer>> mColorModeSpinnerAdapter; + + private Spinner mFittingModeSpinner; + public ArrayAdapter<SpinnerItem<Integer>> mFittingModeSpinnerAdapter; + + private Spinner mOrientationSpinner; + public ArrayAdapter<SpinnerItem<Integer>> mOrientationSpinnerAdapter; + + private boolean mPrintStarted; + + private boolean mPrintConfirmed; + + private IBinder mPrinable; + + // TODO: Implement store/restore state. + + private final OnItemSelectedListener mOnItemSelectedListener = + new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) { + if (spinner == mDestinationSpinner) { + updateUi(); + notifyPrintableStartIfNeeded(); + } else if (spinner == mMediaSizeSpinner) { + SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position); + mPrintAttributes.setMediaSize(mediaItem.value); + updatePrintableContentIfNeeded(); + } else if (spinner == mResolutionSpinner) { + SpinnerItem<Resolution> resolutionItem = + mResolutionSpinnerAdapter.getItem(position); + mPrintAttributes.setResolution(resolutionItem.value); + updatePrintableContentIfNeeded(); + } else if (spinner == mInputTraySpinner) { + SpinnerItem<Tray> inputTrayItem = + mInputTraySpinnerAdapter.getItem(position); + mPrintAttributes.setInputTray(inputTrayItem.value); + } else if (spinner == mOutputTraySpinner) { + SpinnerItem<Tray> outputTrayItem = + mOutputTraySpinnerAdapter.getItem(position); + mPrintAttributes.setOutputTray(outputTrayItem.value); + } else if (spinner == mDuplexModeSpinner) { + SpinnerItem<Integer> duplexModeItem = + mDuplexModeSpinnerAdapter.getItem(position); + mPrintAttributes.setDuplexMode(duplexModeItem.value); + } else if (spinner == mColorModeSpinner) { + SpinnerItem<Integer> colorModeItem = + mColorModeSpinnerAdapter.getItem(position); + mPrintAttributes.setColorMode(colorModeItem.value); + } else if (spinner == mFittingModeSpinner) { + SpinnerItem<Integer> fittingModeItem = + mFittingModeSpinnerAdapter.getItem(position); + mPrintAttributes.setFittingMode(fittingModeItem.value); + } else if (spinner == mOrientationSpinner) { + SpinnerItem<Integer> orientationItem = + mOrientationSpinnerAdapter.getItem(position); + mPrintAttributes.setOrientation(orientationItem.value); + } + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + /* do nothing*/ + } + }; + + private final TextWatcher mTextWatcher = new TextWatcher() { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + final int copies = Integer.parseInt(mCopiesEditText.getText().toString()); + mPrintAttributes.setCopies(copies); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + /* do nothing */ + } + + @Override + public void afterTextChanged(Editable s) { + /* do nothing */ + } + }; + + private final InputFilter mInputFilter = new InputFilter() { + @Override + public CharSequence filter(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend) { + StringBuffer text = new StringBuffer(dest.toString()); + text.replace(dstart, dend, source.subSequence(start, end).toString()); + if (TextUtils.isEmpty(text)) { + return dest; + } + final int copies = Integer.parseInt(text.toString()); + if (copies < MIN_COPIES) { + return dest; + } + return null; + } + }; + + private final DeathRecipient mDeathRecipient = new DeathRecipient() { + @Override + public void binderDied() { + finish(); + } + }; + + @Override + protected void onCreate(Bundle bundle) { + super.onCreate(bundle); + setContentView(R.layout.print_job_config_activity); + + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN + | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + + mPrintManager = (IPrintManager) IPrintManager.Stub.asInterface( + ServiceManager.getService(PRINT_SERVICE)); + + Bundle extras = getIntent().getExtras(); + + mPrintJobId = extras.getInt(EXTRA_PRINT_JOB_ID, -1); + if (mPrintJobId < 0) { + throw new IllegalArgumentException("Invalid print job id: " + mPrintJobId); + } + + mAppId = extras.getInt(EXTRA_APP_ID, -1); + if (mAppId < 0) { + throw new IllegalArgumentException("Invalid app id: " + mAppId); + } + + mPrintAttributes = getIntent().getParcelableExtra(EXTRA_ATTRIBUTES); + if (mPrintAttributes == null) { + mPrintAttributes = new PrintAttributes.Builder().create(); + } + + mPrinable = extras.getBinder(EXTRA_PRINTABLE); + if (mPrinable == null) { + throw new IllegalArgumentException("Printable cannot be null"); + } + mRemotePrintAdapter = new RemotePrintAdapter(IPrintAdapter.Stub.asInterface(mPrinable), + mPrintSpooler.generateFileForPrintJob(mPrintJobId)); + + try { + mPrinable.linkToDeath(mDeathRecipient, 0); + } catch (RemoteException re) { + finish(); + } + + mPrinterDiscoveryObserver = new PrintDiscoveryObserver(getMainLooper()); + + bindUi(); + } + + @Override + protected void onDestroy() { + mPrinable.unlinkToDeath(mDeathRecipient, 0); + super.onDestroy(); + } + + private void bindUi() { + // Copies + mCopiesEditText = (EditText) findViewById(R.id.copies_edittext); + mCopiesEditText.setText(String.valueOf(MIN_COPIES)); + mCopiesEditText.addTextChangedListener(mTextWatcher); + mCopiesEditText.setFilters(new InputFilter[] {mInputFilter}); + + // Destination. + mDestinationSpinner = (Spinner) findViewById(R.id.destination_spinner); + mDestinationSpinnerAdapter = new ArrayAdapter<SpinnerItem<PrinterInfo>>(this, + android.R.layout.simple_spinner_dropdown_item); + mDestinationSpinner.setAdapter(mDestinationSpinnerAdapter); + mDestinationSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Media size. + mMediaSizeSpinner = (Spinner) findViewById(R.id.media_size_spinner); + mMediaSizeSpinnerAdapter = new ArrayAdapter<SpinnerItem<MediaSize>>(this, + android.R.layout.simple_spinner_dropdown_item); + mMediaSizeSpinner.setAdapter(mMediaSizeSpinnerAdapter); + mMediaSizeSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Resolution. + mResolutionSpinner = (Spinner) findViewById(R.id.resolution_spinner); + mResolutionSpinnerAdapter = new ArrayAdapter<SpinnerItem<Resolution>>(this, + android.R.layout.simple_spinner_dropdown_item); + mResolutionSpinner.setAdapter(mResolutionSpinnerAdapter); + mResolutionSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Input tray. + mInputTraySpinner = (Spinner) findViewById(R.id.input_tray_spinner); + mInputTraySpinnerAdapter = new ArrayAdapter<SpinnerItem<Tray>>(this, + android.R.layout.simple_spinner_dropdown_item); + mInputTraySpinner.setAdapter(mInputTraySpinnerAdapter); + + // Output tray. + mOutputTraySpinner = (Spinner) findViewById(R.id.output_tray_spinner); + mOutputTraySpinnerAdapter = new ArrayAdapter<SpinnerItem<Tray>>(this, + android.R.layout.simple_spinner_dropdown_item); + mOutputTraySpinner.setAdapter(mOutputTraySpinnerAdapter); + mOutputTraySpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Duplex mode. + mDuplexModeSpinner = (Spinner) findViewById(R.id.duplex_mode_spinner); + mDuplexModeSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(this, + android.R.layout.simple_spinner_dropdown_item); + mDuplexModeSpinner.setAdapter(mDuplexModeSpinnerAdapter); + mDuplexModeSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Color mode. + mColorModeSpinner = (Spinner) findViewById(R.id.color_mode_spinner); + mColorModeSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(this, + android.R.layout.simple_spinner_dropdown_item); + mColorModeSpinner.setAdapter(mColorModeSpinnerAdapter); + mColorModeSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Color mode. + mFittingModeSpinner = (Spinner) findViewById(R.id.fitting_mode_spinner); + mFittingModeSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(this, + android.R.layout.simple_spinner_dropdown_item); + mFittingModeSpinner.setAdapter(mFittingModeSpinnerAdapter); + mFittingModeSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Orientation + mOrientationSpinner = (Spinner) findViewById(R.id.orientation_spinner); + mOrientationSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(this, + android.R.layout.simple_spinner_dropdown_item); + mOrientationSpinner.setAdapter(mOrientationSpinnerAdapter); + mOrientationSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + } + + private void updateUi() { + final int selectedIndex = mDestinationSpinner.getSelectedItemPosition(); + PrinterInfo printer = mDestinationSpinnerAdapter.getItem(selectedIndex).value; + printer.getDefaults(mPrintAttributes); + + // Copies. + mCopiesEditText.setText(String.valueOf( + Math.max(mPrintAttributes.getCopies(), MIN_COPIES))); + + // Media size. + mMediaSizeSpinnerAdapter.clear(); + List<MediaSize> mediaSizes = printer.getMediaSizes(); + final int mediaSizeCount = mediaSizes.size(); + for (int i = 0; i < mediaSizeCount; i++) { + MediaSize mediaSize = mediaSizes.get(i); + mMediaSizeSpinnerAdapter.add(new SpinnerItem<MediaSize>( + mediaSize, mediaSize.getLabel(getPackageManager()))); + } + final int selectedMediaSizeIndex = mediaSizes.indexOf( + mPrintAttributes.getMediaSize()); + mMediaSizeSpinner.setOnItemSelectedListener(null); + mMediaSizeSpinner.setSelection(selectedMediaSizeIndex); + mMediaSizeSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Resolution. + mResolutionSpinnerAdapter.clear(); + List<Resolution> resolutions = printer.getResolutions(); + final int resolutionCount = resolutions.size(); + for (int i = 0; i < resolutionCount; i++) { + Resolution resolution = resolutions.get(i); + mResolutionSpinnerAdapter.add(new SpinnerItem<Resolution>( + resolution, resolution.getLabel(getPackageManager()))); + } + final int selectedResolutionIndex = resolutions.indexOf( + mPrintAttributes.getResolution()); + mResolutionSpinner.setOnItemSelectedListener(null); + mResolutionSpinner.setSelection(selectedResolutionIndex); + mResolutionSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Input tray. + mInputTraySpinnerAdapter.clear(); + List<Tray> inputTrays = printer.getInputTrays(); + final int inputTrayCount = inputTrays.size(); + for (int i = 0; i < inputTrayCount; i++) { + Tray inputTray = inputTrays.get(i); + mInputTraySpinnerAdapter.add(new SpinnerItem<Tray>( + inputTray, inputTray.getLabel(getPackageManager()))); + } + final int selectedInputTrayIndex = inputTrays.indexOf( + mPrintAttributes.getInputTray()); + mInputTraySpinner.setSelection(selectedInputTrayIndex); + + // Output tray. + mOutputTraySpinnerAdapter.clear(); + List<Tray> outputTrays = printer.getOutputTrays(); + final int outputTrayCount = outputTrays.size(); + for (int i = 0; i < outputTrayCount; i++) { + Tray outputTray = outputTrays.get(i); + mOutputTraySpinnerAdapter.add(new SpinnerItem<Tray>( + outputTray, outputTray.getLabel(getPackageManager()))); + } + final int selectedOutputTrayIndex = outputTrays.indexOf( + mPrintAttributes.getOutputTray()); + mOutputTraySpinner.setSelection(selectedOutputTrayIndex); + + // Duplex mode. + final int duplexModes = printer.getDuplexModes(); + mDuplexModeSpinnerAdapter.clear(); + String[] duplexModeLabels = getResources().getStringArray( + R.array.duplex_mode_labels); + int remainingDuplexModes = duplexModes; + while (remainingDuplexModes != 0) { + final int duplexBitOffset = Integer.numberOfTrailingZeros(remainingDuplexModes); + final int duplexMode = 1 << duplexBitOffset; + remainingDuplexModes &= ~duplexMode; + mDuplexModeSpinnerAdapter.add(new SpinnerItem<Integer>(duplexMode, + duplexModeLabels[duplexBitOffset])); + } + final int selectedDuplexModeIndex = Integer.numberOfTrailingZeros( + (duplexModes & mPrintAttributes.getDuplexMode())); + mDuplexModeSpinner.setSelection(selectedDuplexModeIndex); + + // Color mode. + final int colorModes = printer.getColorModes(); + mColorModeSpinnerAdapter.clear(); + String[] colorModeLabels = getResources().getStringArray( + R.array.color_mode_labels); + int remainingColorModes = colorModes; + while (remainingColorModes != 0) { + final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes); + final int colorMode = 1 << colorBitOffset; + remainingColorModes &= ~colorMode; + mColorModeSpinnerAdapter.add(new SpinnerItem<Integer>(colorMode, + colorModeLabels[colorBitOffset])); + } + final int selectedColorModeIndex = Integer.numberOfTrailingZeros( + (colorModes & mPrintAttributes.getColorMode())); + mColorModeSpinner.setSelection(selectedColorModeIndex); + + // Fitting mode. + final int fittingModes = printer.getFittingModes(); + mFittingModeSpinnerAdapter.clear(); + String[] fittingModeLabels = getResources().getStringArray( + R.array.fitting_mode_labels); + int remainingFittingModes = fittingModes; + while (remainingFittingModes != 0) { + final int fittingBitOffset = Integer.numberOfTrailingZeros(remainingFittingModes); + final int fittingMode = 1 << fittingBitOffset; + remainingFittingModes &= ~fittingMode; + mFittingModeSpinnerAdapter.add(new SpinnerItem<Integer>(fittingMode, + fittingModeLabels[fittingBitOffset])); + } + final int selectedFittingModeIndex = Integer.numberOfTrailingZeros( + (fittingModes & mPrintAttributes.getFittingMode())); + mFittingModeSpinner.setSelection(selectedFittingModeIndex); + + // Orientation. + final int orientations = printer.getOrientations(); + mOrientationSpinnerAdapter.clear(); + String[] orientationLabels = getResources().getStringArray( + R.array.orientation_labels); + int remainingOrientations = orientations; + while (remainingOrientations != 0) { + final int orientationBitOffset = Integer.numberOfTrailingZeros(remainingOrientations); + final int orientation = 1 << orientationBitOffset; + remainingOrientations &= ~orientation; + mOrientationSpinnerAdapter.add(new SpinnerItem<Integer>(orientation, + orientationLabels[orientationBitOffset])); + } + final int selectedOrientationIndex = Integer.numberOfTrailingZeros( + (orientations & mPrintAttributes.getOrientation())); + mOrientationSpinner.setSelection(selectedOrientationIndex); + } + + @Override + protected void onResume() { + super.onResume(); + try { + mPrintManager.startDiscoverPrinters(mPrinterDiscoveryObserver); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error starting printer discovery!", re); + } + notifyPrintableStartIfNeeded(); + } + + @Override + protected void onPause() { + super.onPause(); + try { + mPrintManager.stopDiscoverPrinters(); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error starting printer discovery!", re); + } + notifyPrintableFinishIfNeeded(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.print_job_config_activity, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.print_button) { + mPrintConfirmed = true; + finish(); + } + return super.onOptionsItemSelected(item); + } + + private void notifyPrintableStartIfNeeded() { + if (mDestinationSpinner.getSelectedItemPosition() < 0 + || mPrintStarted) { + return; + } + mPrintStarted = true; + new QueuedAsyncTask<Void>(mTaskQueue) { + @Override + protected Void doInBackground(Void... params) { + try { + mRemotePrintAdapter.start(); + } catch (IOException ioe) { + Log.e(LOG_TAG, "Error reading printed data!", ioe); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + updatePrintableContentIfNeeded(); + } + }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); + } + + private void updatePrintableContentIfNeeded() { + if (!mPrintStarted) { + return; + } + + mPrintSpooler.setPrintJobAttributes(mPrintJobId, mPrintAttributes); + + // TODO: Implement page selector. + final List<PageRange> pages = new ArrayList<PageRange>(); + pages.add(PageRange.ALL_PAGES); + + new QueuedAsyncTask<File>(mTaskQueue) { + @Override + protected File doInBackground(Void... params) { + try { + mRemotePrintAdapter.printAttributesChanged(mPrintAttributes); + mRemotePrintAdapter.cancelPrint(); + mRemotePrintAdapter.print(pages); + return mRemotePrintAdapter.getFile(); + } catch (IOException ioe) { + Log.e(LOG_TAG, "Error reading printed data!", ioe); + } + return null; + } + + @Override + protected void onPostExecute(File file) { + super.onPostExecute(file); + updatePrintPreview(file); + } + }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); + } + + private void notifyPrintableFinishIfNeeded() { + if (!mPrintStarted) { + return; + } + mPrintStarted = false; + + // Cancel all pending async tasks if the activity was canceled. + if (!mPrintConfirmed) { + final int taskCount = mTaskQueue.size(); + for (int i = taskCount - 1; i >= 0; i--) { + mTaskQueue.remove(i).cancel(); + } + } + + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + // Notify the app that printing completed. + try { + mRemotePrintAdapter.finish(); + } catch (IOException ioe) { + Log.e(LOG_TAG, "Error reading printed data!", ioe); + } + + // If canceled, nothing to do. + if (!mPrintConfirmed) { + mPrintSpooler.setPrintJobState(mPrintJobId, + PrintJobInfo.STATE_CANCELED); + return null; + } + + // No printer, nothing to do. + final int selectedIndex = mDestinationSpinner.getSelectedItemPosition(); + if (selectedIndex < 0) { + // Update the print job's status. + mPrintSpooler.setPrintJobState(mPrintJobId, + PrintJobInfo.STATE_CANCELED); + return null; + } + + // Update the print job's printer. + SpinnerItem<PrinterInfo> printerItem = + mDestinationSpinnerAdapter.getItem(selectedIndex); + PrinterId printerId = printerItem.value.getId(); + mPrintSpooler.setPrintJobPrinterId(mPrintJobId, printerId); + + // Update the print job's status. + mPrintSpooler.setPrintJobState(mPrintJobId, + PrintJobInfo.STATE_QUEUED); + return null; + } + + // Important: If we are canceling, then we do not wait for the write + // to complete since the result will be discarded anyway, we simply + // execute the finish immediately which will interrupt the write. + }.executeOnExecutor(mPrintConfirmed ? AsyncTask.SERIAL_EXECUTOR + : AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); + + if (DEBUG) { + if (mPrintConfirmed) { + File file = mRemotePrintAdapter.getFile(); + if (file.exists()) { + new ViewSpooledFileAsyncTask(file).executeOnExecutor( + AsyncTask.SERIAL_EXECUTOR, (Void[]) null); + } + } + } + } + + private void updatePrintPreview(File file) { + // TODO: Implement + } + + private void addPrinters(List<PrinterInfo> addedPrinters) { + final int addedPrinterCount = addedPrinters.size(); + for (int i = 0; i < addedPrinterCount; i++) { + PrinterInfo addedPrinter = addedPrinters.get(i); + boolean duplicate = false; + final int existingPrinterCount = mDestinationSpinnerAdapter.getCount(); + for (int j = 0; j < existingPrinterCount; j++) { + PrinterInfo existingPrinter = mDestinationSpinnerAdapter.getItem(j).value; + if (addedPrinter.getId().equals(existingPrinter.getId())) { + duplicate = true; + break; + } + } + if (!duplicate) { + mDestinationSpinnerAdapter.add(new SpinnerItem<PrinterInfo>( + addedPrinter, addedPrinter.getLabel())); + } else { + Log.w(LOG_TAG, "Skipping a duplicate printer: " + addedPrinter); + } + } + } + + private void removePrinters(List<PrinterId> pritnerIds) { + final int printerIdCount = pritnerIds.size(); + for (int i = 0; i < printerIdCount; i++) { + PrinterId removedPrinterId = pritnerIds.get(i); + boolean removed = false; + final int existingPrinterCount = mDestinationSpinnerAdapter.getCount(); + for (int j = 0; j < existingPrinterCount; j++) { + PrinterInfo existingPrinter = mDestinationSpinnerAdapter.getItem(j).value; + if (removedPrinterId.equals(existingPrinter.getId())) { + mDestinationSpinnerAdapter.remove(mDestinationSpinnerAdapter.getItem(j)); + removed = true; + break; + } + } + if (!removed) { + Log.w(LOG_TAG, "Ignoring not added printer with id: " + removedPrinterId); + } + } + } + + private abstract class QueuedAsyncTask<T> extends AsyncTask<Void, Void, T> { + + private final List<QueuedAsyncTask<?>> mPendingOrRunningTasks; + + public QueuedAsyncTask(List<QueuedAsyncTask<?>> pendingOrRunningTasks) { + mPendingOrRunningTasks = pendingOrRunningTasks; + } + + @Override + protected void onPreExecute() { + mPendingOrRunningTasks.add(this); + } + + @Override + protected void onPostExecute(T result) { + mPendingOrRunningTasks.remove(this); + } + + public void cancel() { + super.cancel(true); + mPendingOrRunningTasks.remove(this); + } + } + + private final class ViewSpooledFileAsyncTask extends AsyncTask<Void, Void, Void> { + + private final File mFile; + + public ViewSpooledFileAsyncTask(File file) { + mFile = file; + } + + @Override + protected Void doInBackground(Void... params) { + mFile.setExecutable(true, false); + mFile.setWritable(true, false); + mFile.setReadable(true, false); + + final long identity = Binder.clearCallingIdentity(); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.fromFile(mFile), "application/pdf"); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivityAsUser(intent, null, UserHandle.CURRENT); + Binder.restoreCallingIdentity(identity); + return null; + } + } + + private final class PrintDiscoveryObserver extends IPrinterDiscoveryObserver.Stub { + private static final int MESSAGE_ADD_DICOVERED_PRINTERS = 1; + private static final int MESSAGE_REMOVE_DICOVERED_PRINTERS = 2; + + private final Handler mHandler; + + @SuppressWarnings("unchecked") + public PrintDiscoveryObserver(Looper looper) { + mHandler = new Handler(looper, null, true) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MESSAGE_ADD_DICOVERED_PRINTERS: { + List<PrinterInfo> printers = (List<PrinterInfo>) message.obj; + addPrinters(printers); + // Just added the first printer, so select it and start printing. + if (mDestinationSpinnerAdapter.getCount() == 1) { + mDestinationSpinner.setSelection(0); + } + } break; + case MESSAGE_REMOVE_DICOVERED_PRINTERS: { + List<PrinterId> printerIds = (List<PrinterId>) message.obj; + removePrinters(printerIds); + // TODO: Handle removing the last printer. + } break; + } + } + }; + } + + @Override + public void addDiscoveredPrinters(List<PrinterInfo> printers) { + mHandler.obtainMessage(MESSAGE_ADD_DICOVERED_PRINTERS, printers).sendToTarget(); + } + + @Override + public void removeDiscoveredPrinters(List<PrinterId> printers) { + mHandler.obtainMessage(MESSAGE_REMOVE_DICOVERED_PRINTERS, printers).sendToTarget(); + } + } + + private final class SpinnerItem<T> { + final T value; + CharSequence label; + + public SpinnerItem(T value, CharSequence label) { + this.value = value; + this.label = label; + } + + public String toString() { + return label.toString(); + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/PrintSpooler.java b/packages/PrintSpooler/src/com/android/printspooler/PrintSpooler.java new file mode 100644 index 0000000..2b27b69 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/PrintSpooler.java @@ -0,0 +1,734 @@ +/* + * Copyright (C) 2013 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; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import android.content.ComponentName; +import android.content.Context; +import android.os.AsyncTask; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.print.IPrintClient; +import android.print.IPrintManager; +import android.print.PrintAttributes; +import android.print.PrintJobInfo; +import android.print.PrintManager; +import android.print.PrinterId; +import android.util.AtomicFile; +import android.util.Log; +import android.util.Slog; +import android.util.Xml; + +import com.android.internal.util.FastXmlSerializer; + +public class PrintSpooler { + + private static final String LOG_TAG = PrintSpooler.class.getSimpleName(); + + private static final boolean DEBUG_PRINT_JOB_LIFECYCLE = false; + + private static final boolean DEBUG_PERSISTENCE = false; + + private static final boolean PERSISTNECE_MANAGER_ENABLED = false; + + private static final String PRINT_FILE_EXTENSION = "pdf"; + + private static int sPrintJobIdCounter; + + private static final Object sLock = new Object(); + + private final Object mLock = new Object(); + + private static PrintSpooler sInstance; + + private final List<PrintJobInfo> mPrintJobs = new ArrayList<PrintJobInfo>(); + + private final PersistenceManager mPersistanceManager; + + private final Context mContext; + + private final IPrintManager mPrintManager; + + public static PrintSpooler getInstance(Context context) { + synchronized (sLock) { + if (sInstance == null) { + sInstance = new PrintSpooler(context); + } + return sInstance; + } + } + + private PrintSpooler(Context context) { + mContext = context; + mPersistanceManager = new PersistenceManager(); + mPersistanceManager.readStateLocked(); + mPrintManager = IPrintManager.Stub.asInterface( + ServiceManager.getService("print")); + } + + public List<PrintJobInfo> getPrintJobs(ComponentName componentName, int state, int appId) { + synchronized (mLock) { + List<PrintJobInfo> foundPrintJobs = null; + final int printJobCount = mPrintJobs.size(); + for (int i = 0; i < printJobCount; i++) { + PrintJobInfo printJob = mPrintJobs.get(i); + PrinterId printerId = printJob.getPrinterId(); + final boolean sameComponent = (componentName == null + || (printerId != null + && componentName.equals(printerId.getServiceComponentName()))); + final boolean sameAppId = appId == PrintManager.APP_ID_ANY + || printJob.getAppId() == appId; + final boolean sameState = state == PrintJobInfo.STATE_ANY + || state == printJob.getState(); + if (sameComponent && sameAppId && sameState) { + if (foundPrintJobs == null) { + foundPrintJobs = new ArrayList<PrintJobInfo>(); + } + foundPrintJobs.add(printJob); + } + } + return foundPrintJobs; + } + } + + public PrintJobInfo getPrintJob(int printJobId, int appId) { + synchronized (mLock) { + final int printJobCount = mPrintJobs.size(); + for (int i = 0; i < printJobCount; i++) { + PrintJobInfo printJob = mPrintJobs.get(i); + if (printJob.getId() == printJobId + && (appId == PrintManager.APP_ID_ANY || appId == printJob.getAppId())) { + return printJob; + } + } + return null; + } + } + + public boolean cancelPrintJob(int printJobId, int appId) { + synchronized (mLock) { + PrintJobInfo printJob = getPrintJob(printJobId, appId); + if (printJob != null) { + switch (printJob.getState()) { + case PrintJobInfo.STATE_CREATED: { + removePrintJobLocked(printJob); + } return true; + case PrintJobInfo.STATE_QUEUED: { + removePrintJobLocked(printJob); + } return true; + default: { + return false; + } + } + } + return false; + } + } + + public PrintJobInfo createPrintJob(CharSequence label, IPrintClient client, + PrintAttributes attributes, int appId) { + synchronized (mLock) { + final int printJobId = generatePrintJobIdLocked(); + PrintJobInfo printJob = new PrintJobInfo(); + printJob.setId(printJobId); + printJob.setAppId(appId); + printJob.setLabel(label); + printJob.setAttributes(attributes); + + addPrintJobLocked(printJob); + setPrintJobState(printJobId, PrintJobInfo.STATE_CREATED); + + return printJob; + } + } + + private int generatePrintJobIdLocked() { + int printJobId = sPrintJobIdCounter++; + while (isDuplicatePrintJobId(printJobId)) { + printJobId = sPrintJobIdCounter++; + } + return printJobId; + } + + private boolean isDuplicatePrintJobId(int printJobId) { + final int printJobCount = mPrintJobs.size(); + for (int j = 0; j < printJobCount; j++) { + PrintJobInfo printJob = mPrintJobs.get(j); + if (printJob.getId() == printJobId) { + return true; + } + } + return false; + } + + @SuppressWarnings("resource") + public boolean writePrintJobData(ParcelFileDescriptor fd, int printJobId) { + synchronized (mLock) { + FileInputStream in = null; + FileOutputStream out = null; + try { + PrintJobInfo printJob = getPrintJob(printJobId, PrintManager.APP_ID_ANY); + if (printJob != null) { + File file = generateFileForPrintJob(printJobId); + in = new FileInputStream(file); + out = new FileOutputStream(fd.getFileDescriptor()); + final byte[] buffer = new byte[8192]; + while (true) { + final int readByteCount = in.read(buffer); + if (readByteCount < 0) { + return true; + } + out.write(buffer, 0, readByteCount); + } + } + } catch (FileNotFoundException fnfe) { + Log.e(LOG_TAG, "Error writing print job data!", fnfe); + } catch (IOException ioe) { + Log.e(LOG_TAG, "Error writing print job data!", ioe); + } finally { + closeIfNotNullNoException(in); + closeIfNotNullNoException(out); + closeIfNotNullNoException(fd); + } + } + return false; + } + + private void closeIfNotNullNoException(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException ioe) { + /* ignore */; + } + } + } + + public File generateFileForPrintJob(int printJobId) { + return new File(mContext.getFilesDir(), "print_job_" + + printJobId + "." + PRINT_FILE_EXTENSION); + } + + private void addPrintJobLocked(PrintJobInfo printJob) { + mPrintJobs.add(printJob); + if (DEBUG_PRINT_JOB_LIFECYCLE) { + Slog.i(LOG_TAG, "[ADD] " + printJob); + } + } + + private void removePrintJobLocked(PrintJobInfo printJob) { + if (mPrintJobs.remove(printJob)) { + generateFileForPrintJob(printJob.getId()).delete(); + if (DEBUG_PRINT_JOB_LIFECYCLE) { + Slog.i(LOG_TAG, "[REMOVE] " + printJob); + } + } + } + + public boolean setPrintJobState(int printJobId, int state) { + boolean success = false; + PrintJobInfo queuedPrintJob = null; + + synchronized (mLock) { + PrintJobInfo printJob = getPrintJob(printJobId, PrintManager.APP_ID_ANY); + if (printJob != null && printJob.getState() < state) { + success = true; + printJob.setState(state); + // TODO: Update notifications. + switch (state) { + case PrintJobInfo.STATE_COMPLETED: + case PrintJobInfo.STATE_CANCELED: { + removePrintJobLocked(printJob); + } break; + case PrintJobInfo.STATE_QUEUED: { + queuedPrintJob = new PrintJobInfo(printJob); + } break; + } + if (DEBUG_PRINT_JOB_LIFECYCLE) { + Slog.i(LOG_TAG, "[STATUS CHANGED] " + printJob); + } + mPersistanceManager.writeStateLocked(); + } + } + + if (queuedPrintJob != null) { + try { + mPrintManager.onPrintJobQueued(queuedPrintJob.getPrinterId(), + queuedPrintJob); + } catch (RemoteException re) { + /* ignore */ + } + } + + return success; + } + + public boolean setPrintJobTag(int printJobId, String tag) { + synchronized (mLock) { + PrintJobInfo printJob = getPrintJob(printJobId, PrintManager.APP_ID_ANY); + if (printJob != null) { + printJob.setTag(tag); + mPersistanceManager.writeStateLocked(); + return true; + } + } + return false; + } + + public void setPrintJobAttributes(int printJobId, PrintAttributes attributes) { + synchronized (mLock) { + PrintJobInfo printJob = getPrintJob(printJobId, PrintManager.APP_ID_ANY); + if (printJob != null) { + printJob.setAttributes(attributes); + mPersistanceManager.writeStateLocked(); + } + } + } + + public void setPrintJobPrinterId(int printJobId, PrinterId printerId) { + synchronized (mLock) { + PrintJobInfo printJob = getPrintJob(printJobId, PrintManager.APP_ID_ANY); + if (printJob != null) { + printJob.setPrinterId(printerId); + mPersistanceManager.writeStateLocked(); + } + } + } + + private final class PersistenceManager { + private static final String PERSIST_FILE_NAME = "print_spooler_state.xml"; + + private static final String TAG_SPOOLER = "spooler"; + private static final String TAG_JOB = "job"; + private static final String TAG_ID = "id"; + private static final String TAG_TAG = "tag"; + private static final String TAG_APP_ID = "app-id"; + private static final String TAG_STATE = "state"; + private static final String TAG_ATTRIBUTES = "attributes"; + private static final String TAG_LABEL = "label"; + private static final String TAG_PRINTER = "printer"; + + private static final String ATTRIBUTE_MEDIA_SIZE = "mediaSize"; + private static final String ATTRIBUTE_RESOLUTION = "resolution"; + private static final String ATTRIBUTE_MARGINS = "margins"; + private static final String ATTRIBUTE_INPUT_TRAY = "inputTray"; + private static final String ATTRIBUTE_OUTPUT_TRAY = "outputTray"; + private static final String ATTRIBUTE_DUPLEX_MODE = "duplexMode"; + private static final String ATTRIBUTE_COLOR_MODE = "colorMode"; + private static final String ATTRIBUTE_FITTING_MODE = "fittingMode"; + private static final String ATTRIBUTE_ORIENTATION = "orientation"; + + private final AtomicFile mStatePersistFile; + + private boolean mWriteStateScheduled; + + private PersistenceManager() { + mStatePersistFile = new AtomicFile(new File(mContext.getFilesDir(), + PERSIST_FILE_NAME)); + } + + public void writeStateLocked() { + // TODO: Implement persistence of PrintableInfo + if (!PERSISTNECE_MANAGER_ENABLED) { + return; + } + if (mWriteStateScheduled) { + return; + } + mWriteStateScheduled = true; + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + synchronized (mLock) { + mWriteStateScheduled = false; + doWriteStateLocked(); + } + return null; + } + }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); + } + + private void doWriteStateLocked() { + FileOutputStream out = null; + try { + out = mStatePersistFile.startWrite(); + + XmlSerializer serializer = new FastXmlSerializer(); + serializer.setOutput(out, "utf-8"); + serializer.startDocument(null, true); + serializer.startTag(null, TAG_SPOOLER); + + List<PrintJobInfo> printJobs = mPrintJobs; + + final int printJobCount = printJobs.size(); + for (int j = 0; j < printJobCount; j++) { + PrintJobInfo printJob = printJobs.get(j); + + final int state = printJob.getState(); + if (state < PrintJobInfo.STATE_QUEUED + || state > PrintJobInfo.STATE_FAILED) { + continue; + } + + serializer.startTag(null, TAG_JOB); + + serializer.startTag(null, TAG_ID); + serializer.text(String.valueOf(printJob.getId())); + serializer.endTag(null, TAG_ID); + + serializer.startTag(null, TAG_TAG); + serializer.text(printJob.getTag()); + serializer.endTag(null, TAG_TAG); + + serializer.startTag(null, TAG_APP_ID); + serializer.text(String.valueOf(printJob.getAppId())); + serializer.endTag(null, TAG_APP_ID); + + serializer.startTag(null, TAG_LABEL); + serializer.text(printJob.getLabel().toString()); + serializer.endTag(null, TAG_LABEL); + + serializer.startTag(null, TAG_STATE); + serializer.text(String.valueOf(printJob.getState())); + serializer.endTag(null, TAG_STATE); + + serializer.startTag(null, TAG_PRINTER); + serializer.text(printJob.getPrinterId().flattenToString()); + serializer.endTag(null, TAG_PRINTER); + + PrintAttributes attributes = printJob.getAttributes(); + if (attributes != null) { + serializer.startTag(null, TAG_ATTRIBUTES); + + //TODO: Implement persistence of the attributes below. + +// MediaSize mediaSize = attributes.getMediaSize(); +// if (mediaSize != null) { +// serializer.attribute(null, ATTRIBUTE_MEDIA_SIZE, +// mediaSize.flattenToString()); +// } +// +// Resolution resolution = attributes.getResolution(); +// if (resolution != null) { +// serializer.attribute(null, ATTRIBUTE_RESOLUTION, +// resolution.flattenToString()); +// } +// +// Margins margins = attributes.getMargins(); +// if (margins != null) { +// serializer.attribute(null, ATTRIBUTE_MARGINS, +// margins.flattenToString()); +// } +// +// Tray inputTray = attributes.getInputTray(); +// if (inputTray != null) { +// serializer.attribute(null, ATTRIBUTE_INPUT_TRAY, +// inputTray.flattenToString()); +// } +// +// Tray outputTray = attributes.getOutputTray(); +// if (outputTray != null) { +// serializer.attribute(null, ATTRIBUTE_OUTPUT_TRAY, +// outputTray.flattenToString()); +// } + + final int duplexMode = attributes.getDuplexMode(); + if (duplexMode > 0) { + serializer.attribute(null, ATTRIBUTE_DUPLEX_MODE, + String.valueOf(duplexMode)); + } + + final int colorMode = attributes.getColorMode(); + if (colorMode > 0) { + serializer.attribute(null, ATTRIBUTE_COLOR_MODE, + String.valueOf(colorMode)); + } + + final int fittingMode = attributes.getFittingMode(); + if (fittingMode > 0) { + serializer.attribute(null, ATTRIBUTE_FITTING_MODE, + String.valueOf(fittingMode)); + } + + final int orientation = attributes.getOrientation(); + if (orientation > 0) { + serializer.attribute(null, ATTRIBUTE_ORIENTATION, + String.valueOf(orientation)); + } + + serializer.endTag(null, TAG_ATTRIBUTES); + } + + serializer.endTag(null, TAG_JOB); + + if (DEBUG_PERSISTENCE) { + Log.i(LOG_TAG, "[PERSISTED] " + printJob); + } + } + + serializer.endTag(null, TAG_SPOOLER); + serializer.endDocument(); + mStatePersistFile.finishWrite(out); + } catch (IOException e) { + Slog.w(LOG_TAG, "Failed to write state, restoring backup.", e); + mStatePersistFile.failWrite(out); + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException ioe) { + /* ignore */ + } + } + } + } + + public void readStateLocked() { + if (!PERSISTNECE_MANAGER_ENABLED) { + return; + } + FileInputStream in = null; + try { + in = mStatePersistFile.openRead(); + } catch (FileNotFoundException e) { + Log.i(LOG_TAG, "No existing print spooler state."); + return; + } + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(in, null); + parseState(parser); + } catch (IllegalStateException ise) { + Slog.w(LOG_TAG, "Failed parsing " + ise); + } catch (NullPointerException npe) { + Slog.w(LOG_TAG, "Failed parsing " + npe); + } catch (NumberFormatException nfe) { + Slog.w(LOG_TAG, "Failed parsing " + nfe); + } catch (XmlPullParserException xppe) { + Slog.w(LOG_TAG, "Failed parsing " + xppe); + } catch (IOException ioe) { + Slog.w(LOG_TAG, "Failed parsing " + ioe); + } catch (IndexOutOfBoundsException iobe) { + Slog.w(LOG_TAG, "Failed parsing " + iobe); + } finally { + try { + in.close(); + } catch (IOException ioe) { + /* ignore */ + } + } + } + + private void parseState(XmlPullParser parser) + throws IOException, XmlPullParserException { + parser.next(); + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.START_TAG, TAG_SPOOLER); + parser.next(); + + while (parsePrintJob(parser)) { + parser.next(); + } + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_SPOOLER); + } + + private boolean parsePrintJob(XmlPullParser parser) + throws IOException, XmlPullParserException { + skipEmptyTextTags(parser); + if (!accept(parser, XmlPullParser.START_TAG, TAG_JOB)) { + return false; + } + parser.next(); + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.START_TAG, TAG_ID); + parser.next(); + final int printJobId = Integer.parseInt(parser.getText()); + parser.next(); + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_ID); + parser.next(); + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.START_TAG, TAG_TAG); + parser.next(); + String tag = parser.getText(); + parser.next(); + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_TAG); + parser.next(); + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.START_TAG, TAG_APP_ID); + parser.next(); + final int appId = Integer.parseInt(parser.getText()); + parser.next(); + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_APP_ID); + parser.next(); + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.START_TAG, TAG_LABEL); + parser.next(); + String label = parser.getText(); + parser.next(); + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_LABEL); + parser.next(); + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.START_TAG, TAG_STATE); + parser.next(); + final int state = Integer.parseInt(parser.getText()); + parser.next(); + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_STATE); + parser.next(); + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.START_TAG, TAG_PRINTER); + parser.next(); + PrinterId printerId = PrinterId.unflattenFromString(parser.getText()); + parser.next(); + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_PRINTER); + parser.next(); + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.START_TAG, TAG_ATTRIBUTES); + + final int attributeCount = parser.getAttributeCount(); + PrintAttributes attributes = null; + if (attributeCount > 0) { + PrintAttributes.Builder builder = new PrintAttributes.Builder(); + + // TODO: Implement reading of the attributes below. + +// String mediaSize = parser.getAttributeValue(null, ATTRIBUTE_MEDIA_SIZE); +// if (mediaSize != null) { +// builder.setMediaSize(MediaSize.unflattenFromString(mediaSize)); +// } +// +// String resolution = parser.getAttributeValue(null, ATTRIBUTE_RESOLUTION); +// if (resolution != null) { +// builder.setMediaSize(Resolution.unflattenFromString(resolution)); +// } +// +// String margins = parser.getAttributeValue(null, ATTRIBUTE_MARGINS); +// if (margins != null) { +// builder.setMediaSize(Margins.unflattenFromString(margins)); +// } +// +// String inputTray = parser.getAttributeValue(null, ATTRIBUTE_INPUT_TRAY); +// if (inputTray != null) { +// builder.setMediaSize(Tray.unflattenFromString(inputTray)); +// } +// +// String outputTray = parser.getAttributeValue(null, ATTRIBUTE_OUTPUT_TRAY); +// if (outputTray != null) { +// builder.setMediaSize(Tray.unflattenFromString(outputTray)); +// } +// +// String duplexMode = parser.getAttributeValue(null, ATTRIBUTE_DUPLEX_MODE); +// if (duplexMode != null) { +// builder.setDuplexMode(Integer.parseInt(duplexMode)); +// } + + String colorMode = parser.getAttributeValue(null, ATTRIBUTE_COLOR_MODE); + if (colorMode != null) { + builder.setColorMode(Integer.parseInt(colorMode)); + } + + String fittingMode = parser.getAttributeValue(null, ATTRIBUTE_COLOR_MODE); + if (fittingMode != null) { + builder.setFittingMode(Integer.parseInt(fittingMode)); + } + } + parser.next(); + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_ATTRIBUTES); + parser.next(); + + PrintJobInfo printJob = new PrintJobInfo(); + printJob.setId(printJobId); + printJob.setTag(tag); + printJob.setAppId(appId); + printJob.setLabel(label); + printJob.setState(state); + printJob.setAttributes(attributes); + printJob.setPrinterId(printerId); + + mPrintJobs.add(printJob); + + if (DEBUG_PERSISTENCE) { + Log.i(LOG_TAG, "[RESTORED] " + printJob); + } + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_JOB); + + return true; + } + + private void expect(XmlPullParser parser, int type, String tag) + throws IOException, XmlPullParserException { + if (!accept(parser, type, tag)) { + throw new XmlPullParserException("Exepected event: " + type + + " and tag: " + tag + " but got event: " + parser.getEventType() + + " and tag:" + parser.getName()); + } + } + + private void skipEmptyTextTags(XmlPullParser parser) + throws IOException, XmlPullParserException { + while (accept(parser, XmlPullParser.TEXT, null) + && "\n".equals(parser.getText())) { + parser.next(); + } + } + + private boolean accept(XmlPullParser parser, int type, String tag) + throws IOException, XmlPullParserException { + if (parser.getEventType() != type) { + return false; + } + if (tag != null) { + if (!tag.equals(parser.getName())) { + return false; + } + } else if (parser.getName() != null) { + return false; + } + return true; + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/PrintSpoolerService.java b/packages/PrintSpooler/src/com/android/printspooler/PrintSpoolerService.java new file mode 100644 index 0000000..57c4557 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/PrintSpoolerService.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2013 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; + +import java.util.List; + +import android.app.PendingIntent; +import android.app.Service; +import android.content.ComponentName; +import android.content.Intent; +import android.content.IntentSender; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.print.IPrintAdapter; +import android.print.IPrintClient; +import android.print.IPrintSpoolerService; +import android.print.IPrintSpoolerServiceCallbacks; +import android.print.PrintAttributes; +import android.print.PrintJobInfo; +import android.util.Slog; + +import com.android.internal.os.SomeArgs; + +/** + * Service for exposing some of the {@link PrintSpooler} functionality to + * another process. + */ +public final class PrintSpoolerService extends Service { + + private static final String LOG_TAG = "PrintSpoolerService"; + + private Intent mStartPrintJobConfigActivityIntent; + + private PrintSpooler mSpooler; + + private Handler mHanlder; + + @Override + public void onCreate() { + super.onCreate(); + mStartPrintJobConfigActivityIntent = new Intent(PrintSpoolerService.this, + PrintJobConfigActivity.class); + mSpooler = PrintSpooler.getInstance(this); + mHanlder = new MyHandler(getMainLooper()); + } + + @Override + public IBinder onBind(Intent intent) { + return new IPrintSpoolerService.Stub() { + @Override + public void getPrintJobs(IPrintSpoolerServiceCallbacks callback, + ComponentName componentName, int state, int appId, int sequence) + throws RemoteException { + List<PrintJobInfo> printJobs = null; + try { + printJobs = mSpooler.getPrintJobs(componentName, state, appId); + } finally { + callback.onGetPrintJobsResult(printJobs, sequence); + } + } + + @Override + public void getPrintJob(int printJobId, IPrintSpoolerServiceCallbacks callback, + int appId, int sequence) throws RemoteException { + PrintJobInfo printJob = null; + try { + printJob = mSpooler.getPrintJob(printJobId, appId); + } finally { + callback.onGetPrintJobInfoResult(printJob, sequence); + } + } + + @Override + public void cancelPrintJob(int printJobId, IPrintSpoolerServiceCallbacks callback, + int appId, int sequence) throws RemoteException { + boolean success = false; + try { + success = mSpooler.cancelPrintJob(printJobId, appId); + } finally { + callback.onCancelPrintJobResult(success, sequence); + } + } + + @SuppressWarnings("deprecation") + @Override + public void createPrintJob(String printJobName, IPrintClient client, + IPrintAdapter printAdapter, PrintAttributes attributes, + IPrintSpoolerServiceCallbacks callback, int appId, int sequence) + throws RemoteException { + PrintJobInfo printJob = null; + try { + printJob = mSpooler.createPrintJob(printJobName, client, + attributes, appId); + if (printJob != null) { + Intent intent = mStartPrintJobConfigActivityIntent; + intent.putExtra(PrintJobConfigActivity.EXTRA_PRINTABLE, + printAdapter.asBinder()); + intent.putExtra(PrintJobConfigActivity.EXTRA_APP_ID, appId); + intent.putExtra(PrintJobConfigActivity.EXTRA_PRINT_JOB_ID, + printJob.getId()); + intent.putExtra(PrintJobConfigActivity.EXTRA_ATTRIBUTES, attributes); + + IntentSender sender = PendingIntent.getActivity( + PrintSpoolerService.this, 0, intent, PendingIntent.FLAG_ONE_SHOT + | PendingIntent.FLAG_CANCEL_CURRENT).getIntentSender(); + + SomeArgs args = SomeArgs.obtain(); + args.arg1 = client; + args.arg2 = sender; + mHanlder.obtainMessage(0, args).sendToTarget(); + } + } finally { + callback.onCreatePrintJobResult(printJob, sequence); + } + } + + @Override + public void setPrintJobState(int printJobId, int state, + IPrintSpoolerServiceCallbacks callback, int sequece) + throws RemoteException { + boolean success = false; + try { + // TODO: Make sure the clients (print services) can set the state + // only to acceptable ones, e.g. not settings STATE_CREATED. + success = mSpooler.setPrintJobState(printJobId, state); + } finally { + callback.onSetPrintJobStateResult(success, sequece); + } + } + + @Override + public void setPrintJobTag(int printJobId, String tag, + IPrintSpoolerServiceCallbacks callback, int sequece) + throws RemoteException { + boolean success = false; + try { + success = mSpooler.setPrintJobTag(printJobId, tag); + } finally { + callback.onSetPrintJobTagResult(success, sequece); + } + } + + @Override + public void writePrintJobData(ParcelFileDescriptor fd, int printJobId) { + mSpooler.writePrintJobData(fd, printJobId); + } + }; + } + + private static final class MyHandler extends Handler { + + public MyHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message message) { + SomeArgs args = (SomeArgs) message.obj; + IPrintClient client = (IPrintClient) args.arg1; + IntentSender sender = (IntentSender) args.arg2; + args.recycle(); + try { + client.startPrintJobConfigActivity(sender); + } catch (RemoteException re) { + Slog.i(LOG_TAG, "Error starting print job config activity!", re); + } + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/RemotePrintAdapter.java b/packages/PrintSpooler/src/com/android/printspooler/RemotePrintAdapter.java new file mode 100644 index 0000000..7537218 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/RemotePrintAdapter.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2013 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; + +import android.os.ICancellationSignal; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.print.IPrintAdapter; +import android.print.IPrintProgressListener; +import android.print.PageRange; +import android.print.PrintAdapterInfo; +import android.print.PrintAttributes; +import android.util.Log; + +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.util.List; + +/** + * This class represents a remote print adapter instance. + */ +final class RemotePrintAdapter { + private static final String LOG_TAG = "RemotePrintAdapter"; + + private static final boolean DEBUG = true; + + private final Object mLock = new Object(); + + private final IPrintAdapter mRemoteInterface; + + private final File mFile; + + private final IPrintProgressListener mIPrintProgressListener; + + private PrintAdapterInfo mInfo; + + private ICancellationSignal mCancellationSignal; + + private Thread mWriteThread; + + public RemotePrintAdapter(IPrintAdapter printAdatper, File file) { + mRemoteInterface = printAdatper; + mFile = file; + mIPrintProgressListener = new IPrintProgressListener.Stub() { + @Override + public void onWriteStarted(PrintAdapterInfo info, + ICancellationSignal cancellationSignal) { + if (DEBUG) { + Log.i(LOG_TAG, "IPrintProgressListener#onWriteStarted()"); + } + synchronized (mLock) { + mInfo = info; + mCancellationSignal = cancellationSignal; + } + } + + @Override + public void onWriteFinished(List<PageRange> pages) { + if (DEBUG) { + Log.i(LOG_TAG, "IPrintProgressListener#onWriteFinished(" + pages + ")"); + } + synchronized (mLock) { + if (isPrintingLocked()) { + mWriteThread.interrupt(); + mCancellationSignal = null; + } + } + } + + @Override + public void onWriteFailed(CharSequence error) { + if (DEBUG) { + Log.i(LOG_TAG, "IPrintProgressListener#onWriteFailed(" + error + ")"); + } + synchronized (mLock) { + if (isPrintingLocked()) { + mWriteThread.interrupt(); + mCancellationSignal = null; + } + } + } + }; + } + + public File getFile() { + if (DEBUG) { + Log.i(LOG_TAG, "getFile()"); + } + return mFile; + } + + public void start() throws IOException { + if (DEBUG) { + Log.i(LOG_TAG, "start()"); + } + try { + mRemoteInterface.start(); + } catch (RemoteException re) { + throw new IOException("Error reading file", re); + } + } + + public void printAttributesChanged(PrintAttributes attributes) throws IOException { + if (DEBUG) { + Log.i(LOG_TAG, "printAttributesChanged(" + attributes +")"); + } + try { + mRemoteInterface.printAttributesChanged(attributes); + } catch (RemoteException re) { + throw new IOException("Error reading file", re); + } + } + + public void print(List<PageRange> pages) throws IOException { + if (DEBUG) { + Log.i(LOG_TAG, "print(" + pages +")"); + } + InputStream in = null; + OutputStream out = null; + ParcelFileDescriptor source = null; + ParcelFileDescriptor sink = null; + synchronized (mLock) { + mWriteThread = Thread.currentThread(); + } + try { + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + source = pipe[0]; + sink = pipe[1]; + + in = new FileInputStream(source.getFileDescriptor()); + out = new FileOutputStream(mFile); + + // Async call to initiate the other process writing the data. + mRemoteInterface.print(pages, sink, mIPrintProgressListener); + + // Close the source. It is now held by the client. + sink.close(); + sink = null; + + final byte[] buffer = new byte[8192]; + while (true) { + if (Thread.currentThread().isInterrupted()) { + Thread.currentThread().interrupt(); + break; + } + final int readByteCount = in.read(buffer); + if (readByteCount < 0) { + break; + } + out.write(buffer, 0, readByteCount); + } + } catch (RemoteException re) { + throw new IOException("Error reading file", re); + } catch (IOException ioe) { + throw new IOException("Error reading file", ioe); + } finally { + IoUtils.closeQuietly(in); + IoUtils.closeQuietly(out); + IoUtils.closeQuietly(sink); + IoUtils.closeQuietly(source); + } + } + + public void cancelPrint() throws IOException { + if (DEBUG) { + Log.i(LOG_TAG, "cancelPrint()"); + } + synchronized (mLock) { + if (isPrintingLocked()) { + try { + mCancellationSignal.cancel(); + } catch (RemoteException re) { + throw new IOException("Error cancelling print", re); + } + } + } + } + + public void finish() throws IOException { + if (DEBUG) { + Log.i(LOG_TAG, "finish()"); + } + try { + mRemoteInterface.finish(); + } catch (RemoteException re) { + throw new IOException("Error reading file", re); + } + } + + public PrintAdapterInfo getInfo() { + synchronized (mLock) { + return mInfo; + } + } + + private boolean isPrintingLocked() { + return mCancellationSignal != null; + } +} |