2 * Copyright (C) 2014 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.printspooler.ui;
19 import android.app.Activity;
20 import android.app.Fragment;
21 import android.app.FragmentTransaction;
22 import android.content.ActivityNotFoundException;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.ServiceConnection;
27 import android.content.pm.PackageInfo;
28 import android.content.pm.PackageManager.NameNotFoundException;
29 import android.content.pm.ResolveInfo;
30 import android.content.res.Configuration;
31 import android.database.DataSetObserver;
32 import android.graphics.drawable.Drawable;
33 import android.net.Uri;
34 import android.os.AsyncTask;
35 import android.os.Bundle;
36 import android.os.Handler;
37 import android.os.IBinder;
38 import android.os.ParcelFileDescriptor;
39 import android.os.RemoteException;
40 import android.print.IPrintDocumentAdapter;
41 import android.print.PageRange;
42 import android.print.PrintAttributes;
43 import android.print.PrintAttributes.MediaSize;
44 import android.print.PrintAttributes.Resolution;
45 import android.print.PrintDocumentInfo;
46 import android.print.PrintJobInfo;
47 import android.print.PrintManager;
48 import android.print.PrinterCapabilitiesInfo;
49 import android.print.PrinterId;
50 import android.print.PrinterInfo;
51 import android.printservice.PrintService;
52 import android.provider.DocumentsContract;
53 import android.text.Editable;
54 import android.text.TextUtils;
55 import android.text.TextUtils.SimpleStringSplitter;
56 import android.text.TextWatcher;
57 import android.util.ArrayMap;
58 import android.util.Log;
59 import android.view.KeyEvent;
60 import android.view.View;
61 import android.view.View.OnClickListener;
62 import android.view.View.OnFocusChangeListener;
63 import android.view.ViewGroup;
64 import android.view.inputmethod.InputMethodManager;
65 import android.widget.AdapterView;
66 import android.widget.AdapterView.OnItemSelectedListener;
67 import android.widget.ArrayAdapter;
68 import android.widget.BaseAdapter;
69 import android.widget.Button;
70 import android.widget.EditText;
71 import android.widget.ImageView;
72 import android.widget.Spinner;
73 import android.widget.TextView;
75 import com.android.printspooler.R;
76 import com.android.printspooler.model.MutexFileProvider;
77 import com.android.printspooler.model.PrintSpoolerProvider;
78 import com.android.printspooler.model.PrintSpoolerService;
79 import com.android.printspooler.model.RemotePrintDocument;
80 import com.android.printspooler.model.RemotePrintDocument.RemotePrintDocumentInfo;
81 import com.android.printspooler.renderer.IPdfEditor;
82 import com.android.printspooler.renderer.PdfManipulationService;
83 import com.android.printspooler.util.MediaSizeUtils;
84 import com.android.printspooler.util.MediaSizeUtils.MediaSizeComparator;
85 import com.android.printspooler.util.PageRangeUtils;
86 import com.android.printspooler.util.PrintOptionUtils;
87 import com.android.printspooler.widget.PrintContentView;
88 import com.android.printspooler.widget.PrintContentView.OptionsStateChangeListener;
89 import com.android.printspooler.widget.PrintContentView.OptionsStateController;
90 import libcore.io.IoUtils;
91 import libcore.io.Streams;
94 import java.io.FileInputStream;
95 import java.io.FileOutputStream;
96 import java.io.IOException;
97 import java.io.InputStream;
98 import java.io.OutputStream;
99 import java.util.ArrayList;
100 import java.util.Arrays;
101 import java.util.Collection;
102 import java.util.Collections;
103 import java.util.List;
104 import java.util.regex.Matcher;
105 import java.util.regex.Pattern;
107 public class PrintActivity extends Activity implements RemotePrintDocument.UpdateResultCallbacks,
108 PrintErrorFragment.OnActionListener, PageAdapter.ContentCallbacks,
109 OptionsStateChangeListener, OptionsStateController {
110 private static final String LOG_TAG = "PrintActivity";
112 private static final boolean DEBUG = false;
114 public static final String INTENT_EXTRA_PRINTER_ID = "INTENT_EXTRA_PRINTER_ID";
116 private static final String FRAGMENT_TAG = "FRAGMENT_TAG";
118 private static final int ORIENTATION_PORTRAIT = 0;
119 private static final int ORIENTATION_LANDSCAPE = 1;
121 private static final int ACTIVITY_REQUEST_CREATE_FILE = 1;
122 private static final int ACTIVITY_REQUEST_SELECT_PRINTER = 2;
123 private static final int ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS = 3;
125 private static final int DEST_ADAPTER_MAX_ITEM_COUNT = 9;
127 private static final int DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF = Integer.MAX_VALUE;
128 private static final int DEST_ADAPTER_ITEM_ID_ALL_PRINTERS = Integer.MAX_VALUE - 1;
130 private static final int STATE_INITIALIZING = 0;
131 private static final int STATE_CONFIGURING = 1;
132 private static final int STATE_PRINT_CONFIRMED = 2;
133 private static final int STATE_PRINT_CANCELED = 3;
134 private static final int STATE_UPDATE_FAILED = 4;
135 private static final int STATE_CREATE_FILE_FAILED = 5;
136 private static final int STATE_PRINTER_UNAVAILABLE = 6;
137 private static final int STATE_UPDATE_SLOW = 7;
138 private static final int STATE_PRINT_COMPLETED = 8;
140 private static final int UI_STATE_PREVIEW = 0;
141 private static final int UI_STATE_ERROR = 1;
142 private static final int UI_STATE_PROGRESS = 2;
144 private static final int MIN_COPIES = 1;
145 private static final String MIN_COPIES_STRING = String.valueOf(MIN_COPIES);
147 private static final Pattern PATTERN_DIGITS = Pattern.compile("[\\d]+");
149 private static final Pattern PATTERN_ESCAPE_SPECIAL_CHARS = Pattern.compile(
150 "(?=[]\\[+&|!(){}^\"~*?:\\\\])");
152 private static final Pattern PATTERN_PAGE_RANGE = Pattern.compile(
153 "[\\s]*[0-9]+[\\-]?[\\s]*[0-9]*[\\s]*?(([,])"
154 + "[\\s]*[0-9]+[\\s]*[\\-]?[\\s]*[0-9]*[\\s]*|[\\s]*)+");
156 public static final PageRange[] ALL_PAGES_ARRAY = new PageRange[]{PageRange.ALL_PAGES};
158 private final PrinterAvailabilityDetector mPrinterAvailabilityDetector =
159 new PrinterAvailabilityDetector();
161 private final SimpleStringSplitter mStringCommaSplitter = new SimpleStringSplitter(',');
163 private final OnFocusChangeListener mSelectAllOnFocusListener = new SelectAllOnFocusListener();
165 private PrintSpoolerProvider mSpoolerProvider;
167 private PrintPreviewController mPrintPreviewController;
169 private PrintJobInfo mPrintJob;
170 private RemotePrintDocument mPrintedDocument;
171 private PrinterRegistry mPrinterRegistry;
173 private EditText mCopiesEditText;
175 private TextView mPageRangeTitle;
176 private EditText mPageRangeEditText;
178 private Spinner mDestinationSpinner;
179 private DestinationAdapter mDestinationSpinnerAdapter;
181 private Spinner mMediaSizeSpinner;
182 private ArrayAdapter<SpinnerItem<MediaSize>> mMediaSizeSpinnerAdapter;
184 private Spinner mColorModeSpinner;
185 private ArrayAdapter<SpinnerItem<Integer>> mColorModeSpinnerAdapter;
187 private Spinner mOrientationSpinner;
188 private ArrayAdapter<SpinnerItem<Integer>> mOrientationSpinnerAdapter;
190 private Spinner mRangeOptionsSpinner;
192 private PrintContentView mOptionsContent;
194 private View mSummaryContainer;
195 private TextView mSummaryCopies;
196 private TextView mSummaryPaperSize;
198 private Button mMoreOptionsButton;
200 private ImageView mPrintButton;
202 private ProgressMessageController mProgressMessageController;
203 private MutexFileProvider mFileProvider;
205 private MediaSizeComparator mMediaSizeComparator;
207 private PrinterInfo mCurrentPrinter;
209 private PageRange[] mSelectedPages;
211 private String mCallingPackageName;
213 private int mCurrentPageCount;
215 private int mState = STATE_INITIALIZING;
217 private int mUiState = UI_STATE_PREVIEW;
220 public void onCreate(Bundle savedInstanceState) {
221 super.onCreate(savedInstanceState);
223 Bundle extras = getIntent().getExtras();
225 mPrintJob = extras.getParcelable(PrintManager.EXTRA_PRINT_JOB);
226 if (mPrintJob == null) {
227 throw new IllegalArgumentException(PrintManager.EXTRA_PRINT_JOB
228 + " cannot be null");
230 mPrintJob.setAttributes(new PrintAttributes.Builder().build());
232 final IBinder adapter = extras.getBinder(PrintManager.EXTRA_PRINT_DOCUMENT_ADAPTER);
233 if (adapter == null) {
234 throw new IllegalArgumentException(PrintManager.EXTRA_PRINT_DOCUMENT_ADAPTER
235 + " cannot be null");
238 mCallingPackageName = extras.getString(DocumentsContract.EXTRA_PACKAGE_NAME);
240 // This will take just a few milliseconds, so just wait to
241 // bind to the local service before showing the UI.
242 mSpoolerProvider = new PrintSpoolerProvider(this,
246 onConnectedToPrintSpooler(adapter);
251 private void onConnectedToPrintSpooler(final IBinder documentAdapter) {
252 // Now that we are bound to the print spooler service,
253 // create the printer registry and wait for it to get
254 // the first batch of results which will be delivered
255 // after reading historical data. This should be pretty
256 // fast, so just wait before showing the UI.
257 mPrinterRegistry = new PrinterRegistry(PrintActivity.this,
261 onPrinterRegistryReady(documentAdapter);
266 private void onPrinterRegistryReady(IBinder documentAdapter) {
267 // Now that we are bound to the local print spooler service
268 // and the printer registry loaded the historical printers
269 // we can show the UI without flickering.
270 setTitle(R.string.print_dialog);
271 setContentView(R.layout.print_activity);
274 mFileProvider = new MutexFileProvider(
275 PrintSpoolerService.generateFileForPrintJob(
276 PrintActivity.this, mPrintJob.getId()));
277 } catch (IOException ioe) {
278 // At this point we cannot recover, so just take it down.
279 throw new IllegalStateException("Cannot create print job file", ioe);
282 mPrintPreviewController = new PrintPreviewController(PrintActivity.this,
284 mPrintedDocument = new RemotePrintDocument(PrintActivity.this,
285 IPrintDocumentAdapter.Stub.asInterface(documentAdapter),
286 mFileProvider, new RemotePrintDocument.RemoteAdapterDeathObserver() {
288 public void onDied() {
289 // If we are finishing or we are in a state that we do not need any
290 // data from the printing app, then no need to finish.
291 if (isFinishing() || (isFinalState(mState) && !mPrintedDocument.isUpdating())) {
294 if (mPrintedDocument.isUpdating()) {
295 mPrintedDocument.cancel();
297 setState(STATE_PRINT_CANCELED);
300 }, PrintActivity.this);
301 mProgressMessageController = new ProgressMessageController(
303 mMediaSizeComparator = new MediaSizeComparator(PrintActivity.this);
304 mDestinationSpinnerAdapter = new DestinationAdapter();
309 // Now show the updated UI to avoid flicker.
310 mOptionsContent.setVisibility(View.VISIBLE);
311 mSelectedPages = computeSelectedPages();
312 mPrintedDocument.start();
314 ensurePreviewUiShown();
316 setState(STATE_CONFIGURING);
320 public void onResume() {
322 if (mState != STATE_INITIALIZING && mCurrentPrinter != null) {
323 mPrinterRegistry.setTrackedPrinter(mCurrentPrinter.getId());
328 public void onPause() {
329 PrintSpoolerService spooler = mSpoolerProvider.getSpooler();
331 if (mState == STATE_INITIALIZING) {
333 spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_CANCELED, null);
340 spooler.updatePrintJobUserConfigurableOptionsNoPersistence(mPrintJob);
343 case STATE_PRINT_CONFIRMED: {
344 spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_QUEUED, null);
347 case STATE_PRINT_COMPLETED: {
348 spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_COMPLETED, null);
351 case STATE_CREATE_FILE_FAILED: {
352 spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_FAILED,
353 getString(R.string.print_write_error_message));
357 spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_CANCELED, null);
362 mPrinterAvailabilityDetector.cancel();
363 mPrinterRegistry.setTrackedPrinter(null);
369 public boolean onKeyDown(int keyCode, KeyEvent event) {
370 if (keyCode == KeyEvent.KEYCODE_BACK) {
371 event.startTracking();
374 return super.onKeyDown(keyCode, event);
378 public boolean onKeyUp(int keyCode, KeyEvent event) {
379 if (mState == STATE_INITIALIZING) {
384 if (mState == STATE_PRINT_CANCELED || mState == STATE_PRINT_CONFIRMED
385 || mState == STATE_PRINT_COMPLETED) {
389 if (keyCode == KeyEvent.KEYCODE_BACK
390 && event.isTracking() && !event.isCanceled()) {
391 if (mPrintPreviewController != null && mPrintPreviewController.isOptionsOpened()
393 mPrintPreviewController.closeOptions();
399 return super.onKeyUp(keyCode, event);
403 public void onRequestContentUpdate() {
404 if (canUpdateDocument()) {
405 updateDocument(false);
410 public void onMalformedPdfFile() {
411 onPrintDocumentError("Cannot print a malformed PDF file");
415 public void onSecurePdfFile() {
416 onPrintDocumentError("Cannot print a password protected PDF file");
419 private void onPrintDocumentError(String message) {
420 mProgressMessageController.cancel();
421 ensureErrorUiShown(null, PrintErrorFragment.ACTION_RETRY);
423 setState(STATE_UPDATE_FAILED);
427 mPrintedDocument.kill(message);
431 public void onActionPerformed() {
432 if (mState == STATE_UPDATE_FAILED
433 && canUpdateDocument() && updateDocument(true)) {
434 ensurePreviewUiShown();
435 setState(STATE_CONFIGURING);
440 public void onUpdateCanceled() {
442 Log.i(LOG_TAG, "onUpdateCanceled()");
445 mProgressMessageController.cancel();
446 ensurePreviewUiShown();
449 case STATE_PRINT_CONFIRMED: {
450 requestCreatePdfFileOrFinish();
453 case STATE_PRINT_CANCELED: {
460 public void onUpdateCompleted(RemotePrintDocumentInfo document) {
462 Log.i(LOG_TAG, "onUpdateCompleted()");
465 mProgressMessageController.cancel();
466 ensurePreviewUiShown();
468 // Update the print job with the info for the written document. The page
469 // count we get from the remote document is the pages in the document from
470 // the app perspective but the print job should contain the page count from
471 // print service perspective which is the pages in the written PDF not the
472 // pages in the printed document.
473 PrintDocumentInfo info = document.info;
475 final int pageCount = PageRangeUtils.getNormalizedPageCount(document.writtenPages,
476 getAdjustedPageCount(info));
477 PrintDocumentInfo adjustedInfo = new PrintDocumentInfo.Builder(info.getName())
478 .setContentType(info.getContentType())
479 .setPageCount(pageCount)
481 mPrintJob.setDocumentInfo(adjustedInfo);
482 mPrintJob.setPages(document.printedPages);
486 case STATE_PRINT_CONFIRMED: {
487 requestCreatePdfFileOrFinish();
491 updatePrintPreviewController(document.changed);
493 setState(STATE_CONFIGURING);
500 public void onUpdateFailed(CharSequence error) {
502 Log.i(LOG_TAG, "onUpdateFailed()");
505 mProgressMessageController.cancel();
506 ensureErrorUiShown(error, PrintErrorFragment.ACTION_RETRY);
508 setState(STATE_UPDATE_FAILED);
514 public void onOptionsOpened() {
515 updateSelectedPagesFromPreview();
519 public void onOptionsClosed() {
520 PageRange[] selectedPages = computeSelectedPages();
521 if (!Arrays.equals(mSelectedPages, selectedPages)) {
522 mSelectedPages = selectedPages;
525 updatePrintPreviewController(false);
528 // Make sure the IME is not on the way of preview as
529 // the user may have used it to type copies or range.
530 InputMethodManager imm = (InputMethodManager) getSystemService(
531 Context.INPUT_METHOD_SERVICE);
532 imm.hideSoftInputFromWindow(mDestinationSpinner.getWindowToken(), 0);
535 private void updatePrintPreviewController(boolean contentUpdated) {
536 // If we have not heard from the application, do nothing.
537 RemotePrintDocumentInfo documentInfo = mPrintedDocument.getDocumentInfo();
538 if (!documentInfo.laidout) {
542 // Update the preview controller.
543 mPrintPreviewController.onContentUpdated(contentUpdated,
544 getAdjustedPageCount(documentInfo.info),
545 mPrintedDocument.getDocumentInfo().writtenPages,
546 mSelectedPages, mPrintJob.getAttributes().getMediaSize(),
547 mPrintJob.getAttributes().getMinMargins());
552 public boolean canOpenOptions() {
557 public boolean canCloseOptions() {
562 public void onConfigurationChanged(Configuration newConfig) {
563 super.onConfigurationChanged(newConfig);
564 if (mPrintPreviewController != null) {
565 mPrintPreviewController.onOrientationChanged();
570 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
571 switch (requestCode) {
572 case ACTIVITY_REQUEST_CREATE_FILE: {
573 onStartCreateDocumentActivityResult(resultCode, data);
576 case ACTIVITY_REQUEST_SELECT_PRINTER: {
577 onSelectPrinterActivityResult(resultCode, data);
580 case ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS: {
581 onAdvancedPrintOptionsActivityResult(resultCode, data);
586 private void startCreateDocumentActivity() {
590 PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
594 Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
595 intent.setType("application/pdf");
596 intent.putExtra(Intent.EXTRA_TITLE, info.getName());
597 intent.putExtra(DocumentsContract.EXTRA_PACKAGE_NAME, mCallingPackageName);
598 startActivityForResult(intent, ACTIVITY_REQUEST_CREATE_FILE);
601 private void onStartCreateDocumentActivityResult(int resultCode, Intent data) {
602 if (resultCode == RESULT_OK && data != null) {
603 setState(STATE_PRINT_COMPLETED);
605 final Uri uri = data.getData();
606 // Calling finish here does not invoke lifecycle callbacks but we
607 // update the print job in onPause if finishing, hence post a message.
608 mDestinationSpinner.post(new Runnable() {
611 transformDocumentAndFinish(uri);
614 } else if (resultCode == RESULT_CANCELED) {
615 mState = STATE_CONFIGURING;
618 setState(STATE_CREATE_FILE_FAILED);
620 // Calling finish here does not invoke lifecycle callbacks but we
621 // update the print job in onPause if finishing, hence post a message.
622 mDestinationSpinner.post(new Runnable() {
631 private void startSelectPrinterActivity() {
632 Intent intent = new Intent(this, SelectPrinterActivity.class);
633 startActivityForResult(intent, ACTIVITY_REQUEST_SELECT_PRINTER);
636 private void onSelectPrinterActivityResult(int resultCode, Intent data) {
637 if (resultCode == RESULT_OK && data != null) {
638 PrinterId printerId = data.getParcelableExtra(INTENT_EXTRA_PRINTER_ID);
639 if (printerId != null) {
640 mDestinationSpinnerAdapter.ensurePrinterInVisibleAdapterPosition(printerId);
641 final int index = mDestinationSpinnerAdapter.getPrinterIndex(printerId);
642 if (index != AdapterView.INVALID_POSITION) {
643 mDestinationSpinner.setSelection(index);
649 PrinterId printerId = mCurrentPrinter.getId();
650 final int index = mDestinationSpinnerAdapter.getPrinterIndex(printerId);
651 mDestinationSpinner.setSelection(index);
654 private void startAdvancedPrintOptionsActivity(PrinterInfo printer) {
655 ComponentName serviceName = printer.getId().getServiceName();
657 String activityName = PrintOptionUtils.getAdvancedOptionsActivityName(this, serviceName);
658 if (TextUtils.isEmpty(activityName)) {
662 Intent intent = new Intent(Intent.ACTION_MAIN);
663 intent.setComponent(new ComponentName(serviceName.getPackageName(), activityName));
665 List<ResolveInfo> resolvedActivities = getPackageManager()
666 .queryIntentActivities(intent, 0);
667 if (resolvedActivities.isEmpty()) {
671 // The activity is a component name, therefore it is one or none.
672 if (resolvedActivities.get(0).activityInfo.exported) {
673 intent.putExtra(PrintService.EXTRA_PRINT_JOB_INFO, mPrintJob);
674 intent.putExtra(PrintService.EXTRA_PRINTER_INFO, printer);
676 // This is external activity and may not be there.
678 startActivityForResult(intent, ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS);
679 } catch (ActivityNotFoundException anfe) {
680 Log.e(LOG_TAG, "Error starting activity for intent: " + intent, anfe);
685 private void onAdvancedPrintOptionsActivityResult(int resultCode, Intent data) {
686 if (resultCode != RESULT_OK || data == null) {
690 PrintJobInfo printJobInfo = data.getParcelableExtra(PrintService.EXTRA_PRINT_JOB_INFO);
692 if (printJobInfo == null) {
696 // Take the advanced options without interpretation.
697 mPrintJob.setAdvancedOptions(printJobInfo.getAdvancedOptions());
699 // Take copies without interpretation as the advanced print dialog
700 // cannot create a print job info with invalid copies.
701 mCopiesEditText.setText(String.valueOf(printJobInfo.getCopies()));
702 mPrintJob.setCopies(printJobInfo.getCopies());
704 PrintAttributes currAttributes = mPrintJob.getAttributes();
705 PrintAttributes newAttributes = printJobInfo.getAttributes();
707 if (newAttributes != null) {
708 // Take the media size only if the current printer supports is.
709 MediaSize oldMediaSize = currAttributes.getMediaSize();
710 MediaSize newMediaSize = newAttributes.getMediaSize();
711 if (!oldMediaSize.equals(newMediaSize)) {
712 final int mediaSizeCount = mMediaSizeSpinnerAdapter.getCount();
713 MediaSize newMediaSizePortrait = newAttributes.getMediaSize().asPortrait();
714 for (int i = 0; i < mediaSizeCount; i++) {
715 MediaSize supportedSizePortrait = mMediaSizeSpinnerAdapter.getItem(i)
717 if (supportedSizePortrait.equals(newMediaSizePortrait)) {
718 currAttributes.setMediaSize(newMediaSize);
719 mMediaSizeSpinner.setSelection(i);
720 if (currAttributes.getMediaSize().isPortrait()) {
721 if (mOrientationSpinner.getSelectedItemPosition() != 0) {
722 mOrientationSpinner.setSelection(0);
725 if (mOrientationSpinner.getSelectedItemPosition() != 1) {
726 mOrientationSpinner.setSelection(1);
734 // Take the resolution only if the current printer supports is.
735 Resolution oldResolution = currAttributes.getResolution();
736 Resolution newResolution = newAttributes.getResolution();
737 if (!oldResolution.equals(newResolution)) {
738 PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
739 if (capabilities != null) {
740 List<Resolution> resolutions = capabilities.getResolutions();
741 final int resolutionCount = resolutions.size();
742 for (int i = 0; i < resolutionCount; i++) {
743 Resolution resolution = resolutions.get(i);
744 if (resolution.equals(newResolution)) {
745 currAttributes.setResolution(resolution);
752 // Take the color mode only if the current printer supports it.
753 final int currColorMode = currAttributes.getColorMode();
754 final int newColorMode = newAttributes.getColorMode();
755 if (currColorMode != newColorMode) {
756 final int colorModeCount = mColorModeSpinner.getCount();
757 for (int i = 0; i < colorModeCount; i++) {
758 final int supportedColorMode = mColorModeSpinnerAdapter.getItem(i).value;
759 if (supportedColorMode == newColorMode) {
760 currAttributes.setColorMode(newColorMode);
761 mColorModeSpinner.setSelection(i);
768 // Handle selected page changes making sure they are in the doc.
769 PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
770 final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
771 PageRange[] pageRanges = printJobInfo.getPages();
772 if (pageRanges != null && pageCount > 0) {
773 pageRanges = PageRangeUtils.normalize(pageRanges);
775 List<PageRange> validatedList = new ArrayList<>();
776 final int rangeCount = pageRanges.length;
777 for (int i = 0; i < rangeCount; i++) {
778 PageRange pageRange = pageRanges[i];
779 if (pageRange.getEnd() >= pageCount) {
780 final int rangeStart = pageRange.getStart();
781 final int rangeEnd = pageCount - 1;
782 if (rangeStart <= rangeEnd) {
783 pageRange = new PageRange(rangeStart, rangeEnd);
784 validatedList.add(pageRange);
788 validatedList.add(pageRange);
791 if (!validatedList.isEmpty()) {
792 PageRange[] validatedArray = new PageRange[validatedList.size()];
793 validatedList.toArray(validatedArray);
794 updateSelectedPages(validatedArray, pageCount);
798 // Update the content if needed.
799 if (canUpdateDocument()) {
800 updateDocument(false);
804 private void setState(int state) {
805 if (isFinalState(mState)) {
806 if (isFinalState(state)) {
814 private static boolean isFinalState(int state) {
815 return state == STATE_PRINT_CONFIRMED
816 || state == STATE_PRINT_CANCELED
817 || state == STATE_PRINT_COMPLETED;
820 private void updateSelectedPagesFromPreview() {
821 PageRange[] selectedPages = mPrintPreviewController.getSelectedPages();
822 if (!Arrays.equals(mSelectedPages, selectedPages)) {
823 updateSelectedPages(selectedPages,
824 getAdjustedPageCount(mPrintedDocument.getDocumentInfo().info));
828 private void updateSelectedPages(PageRange[] selectedPages, int pageInDocumentCount) {
829 if (selectedPages == null || selectedPages.length <= 0) {
833 selectedPages = PageRangeUtils.normalize(selectedPages);
835 // Handle the case where all pages are specified explicitly
836 // instead of the *all pages* constant.
837 if (PageRangeUtils.isAllPages(selectedPages, pageInDocumentCount)) {
838 selectedPages = new PageRange[] {PageRange.ALL_PAGES};
841 if (Arrays.equals(mSelectedPages, selectedPages)) {
845 mSelectedPages = selectedPages;
846 mPrintJob.setPages(selectedPages);
848 if (Arrays.equals(selectedPages, ALL_PAGES_ARRAY)) {
849 if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
850 mRangeOptionsSpinner.setSelection(0);
851 mPageRangeEditText.setText("");
853 } else if (selectedPages[0].getStart() >= 0
854 && selectedPages[selectedPages.length - 1].getEnd() < pageInDocumentCount) {
855 if (mRangeOptionsSpinner.getSelectedItemPosition() != 1) {
856 mRangeOptionsSpinner.setSelection(1);
859 StringBuilder builder = new StringBuilder();
860 final int pageRangeCount = selectedPages.length;
861 for (int i = 0; i < pageRangeCount; i++) {
862 if (builder.length() > 0) {
866 final int shownStartPage;
867 final int shownEndPage;
868 PageRange pageRange = selectedPages[i];
869 if (pageRange.equals(PageRange.ALL_PAGES)) {
871 shownEndPage = pageInDocumentCount;
873 shownStartPage = pageRange.getStart() + 1;
874 shownEndPage = pageRange.getEnd() + 1;
877 builder.append(shownStartPage);
879 if (shownStartPage != shownEndPage) {
881 builder.append(shownEndPage);
885 mPageRangeEditText.setText(builder.toString());
889 private void ensureProgressUiShown() {
893 if (mUiState != UI_STATE_PROGRESS) {
894 mUiState = UI_STATE_PROGRESS;
895 mPrintPreviewController.setUiShown(false);
896 Fragment fragment = PrintProgressFragment.newInstance();
897 showFragment(fragment);
901 private void ensurePreviewUiShown() {
905 if (mUiState != UI_STATE_PREVIEW) {
906 mUiState = UI_STATE_PREVIEW;
907 mPrintPreviewController.setUiShown(true);
912 private void ensureErrorUiShown(CharSequence message, int action) {
916 if (mUiState != UI_STATE_ERROR) {
917 mUiState = UI_STATE_ERROR;
918 mPrintPreviewController.setUiShown(false);
919 Fragment fragment = PrintErrorFragment.newInstance(message, action);
920 showFragment(fragment);
924 private void showFragment(Fragment newFragment) {
925 FragmentTransaction transaction = getFragmentManager().beginTransaction();
926 Fragment oldFragment = getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
927 if (oldFragment != null) {
928 transaction.remove(oldFragment);
930 if (newFragment != null) {
931 transaction.add(R.id.embedded_content_container, newFragment, FRAGMENT_TAG);
933 transaction.commit();
934 getFragmentManager().executePendingTransactions();
937 private void requestCreatePdfFileOrFinish() {
938 if (mCurrentPrinter == mDestinationSpinnerAdapter.getPdfPrinter()) {
939 startCreateDocumentActivity();
941 transformDocumentAndFinish(null);
945 private void updatePrintAttributesFromCapabilities(PrinterCapabilitiesInfo capabilities) {
946 PrintAttributes defaults = capabilities.getDefaults();
948 // Sort the media sizes based on the current locale.
949 List<MediaSize> sortedMediaSizes = new ArrayList<>(capabilities.getMediaSizes());
950 Collections.sort(sortedMediaSizes, mMediaSizeComparator);
952 PrintAttributes attributes = mPrintJob.getAttributes();
955 MediaSize currMediaSize = attributes.getMediaSize();
956 if (currMediaSize == null) {
957 attributes.setMediaSize(defaults.getMediaSize());
959 boolean foundCurrentMediaSize = false;
960 // Try to find the current media size in the capabilities as
961 // it may be in a different orientation.
962 MediaSize currMediaSizePortrait = currMediaSize.asPortrait();
963 final int mediaSizeCount = sortedMediaSizes.size();
964 for (int i = 0; i < mediaSizeCount; i++) {
965 MediaSize mediaSize = sortedMediaSizes.get(i);
966 if (currMediaSizePortrait.equals(mediaSize.asPortrait())) {
967 attributes.setMediaSize(currMediaSize);
968 foundCurrentMediaSize = true;
972 // If we did not find the current media size fall back to default.
973 if (!foundCurrentMediaSize) {
974 attributes.setMediaSize(defaults.getMediaSize());
979 final int colorMode = attributes.getColorMode();
980 if ((capabilities.getColorModes() & colorMode) == 0) {
981 attributes.setColorMode(defaults.getColorMode());
985 Resolution resolution = attributes.getResolution();
986 if (resolution == null || !capabilities.getResolutions().contains(resolution)) {
987 attributes.setResolution(defaults.getResolution());
991 attributes.setMinMargins(defaults.getMinMargins());
994 private boolean updateDocument(boolean clearLastError) {
995 if (!clearLastError && mPrintedDocument.hasUpdateError()) {
999 if (clearLastError && mPrintedDocument.hasUpdateError()) {
1000 mPrintedDocument.clearUpdateError();
1003 final boolean preview = mState != STATE_PRINT_CONFIRMED;
1004 final PageRange[] pages;
1006 pages = mPrintPreviewController.getRequestedPages();
1008 pages = mPrintPreviewController.getSelectedPages();
1011 final boolean willUpdate = mPrintedDocument.update(mPrintJob.getAttributes(),
1014 if (willUpdate && !mPrintedDocument.hasLaidOutPages()) {
1015 // When the update is done we update the print preview.
1016 mProgressMessageController.post();
1018 } else if (!willUpdate) {
1020 updatePrintPreviewController(false);
1026 private void addCurrentPrinterToHistory() {
1027 if (mCurrentPrinter != null) {
1028 PrinterId fakePdfPrinterId = mDestinationSpinnerAdapter.getPdfPrinter().getId();
1029 if (!mCurrentPrinter.getId().equals(fakePdfPrinterId)) {
1030 mPrinterRegistry.addHistoricalPrinter(mCurrentPrinter);
1035 private void cancelPrint() {
1036 setState(STATE_PRINT_CANCELED);
1038 if (mPrintedDocument.isUpdating()) {
1039 mPrintedDocument.cancel();
1044 private void confirmPrint() {
1045 setState(STATE_PRINT_CONFIRMED);
1048 addCurrentPrinterToHistory();
1050 PageRange[] selectedPages = computeSelectedPages();
1051 if (!Arrays.equals(mSelectedPages, selectedPages)) {
1052 mSelectedPages = selectedPages;
1054 updatePrintPreviewController(false);
1057 updateSelectedPagesFromPreview();
1058 mPrintPreviewController.closeOptions();
1060 if (canUpdateDocument()) {
1061 updateDocument(false);
1064 if (!mPrintedDocument.isUpdating()) {
1065 requestCreatePdfFileOrFinish();
1069 private void bindUi() {
1071 mSummaryContainer = findViewById(R.id.summary_content);
1072 mSummaryCopies = (TextView) findViewById(R.id.copies_count_summary);
1073 mSummaryPaperSize = (TextView) findViewById(R.id.paper_size_summary);
1075 // Options container
1076 mOptionsContent = (PrintContentView) findViewById(R.id.options_content);
1077 mOptionsContent.setOptionsStateChangeListener(this);
1078 mOptionsContent.setOpenOptionsController(this);
1080 OnItemSelectedListener itemSelectedListener = new MyOnItemSelectedListener();
1081 OnClickListener clickListener = new MyClickListener();
1084 mCopiesEditText = (EditText) findViewById(R.id.copies_edittext);
1085 mCopiesEditText.setOnFocusChangeListener(mSelectAllOnFocusListener);
1086 mCopiesEditText.setText(MIN_COPIES_STRING);
1087 mCopiesEditText.setSelection(mCopiesEditText.getText().length());
1088 mCopiesEditText.addTextChangedListener(new EditTextWatcher());
1091 mDestinationSpinnerAdapter.registerDataSetObserver(new PrintersObserver());
1092 mDestinationSpinner = (Spinner) findViewById(R.id.destination_spinner);
1093 mDestinationSpinner.setAdapter(mDestinationSpinnerAdapter);
1094 mDestinationSpinner.setOnItemSelectedListener(itemSelectedListener);
1097 mMediaSizeSpinnerAdapter = new ArrayAdapter<>(
1098 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1099 mMediaSizeSpinner = (Spinner) findViewById(R.id.paper_size_spinner);
1100 mMediaSizeSpinner.setAdapter(mMediaSizeSpinnerAdapter);
1101 mMediaSizeSpinner.setOnItemSelectedListener(itemSelectedListener);
1104 mColorModeSpinnerAdapter = new ArrayAdapter<>(
1105 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1106 mColorModeSpinner = (Spinner) findViewById(R.id.color_spinner);
1107 mColorModeSpinner.setAdapter(mColorModeSpinnerAdapter);
1108 mColorModeSpinner.setOnItemSelectedListener(itemSelectedListener);
1111 mOrientationSpinnerAdapter = new ArrayAdapter<>(
1112 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1113 String[] orientationLabels = getResources().getStringArray(
1114 R.array.orientation_labels);
1115 mOrientationSpinnerAdapter.add(new SpinnerItem<>(
1116 ORIENTATION_PORTRAIT, orientationLabels[0]));
1117 mOrientationSpinnerAdapter.add(new SpinnerItem<>(
1118 ORIENTATION_LANDSCAPE, orientationLabels[1]));
1119 mOrientationSpinner = (Spinner) findViewById(R.id.orientation_spinner);
1120 mOrientationSpinner.setAdapter(mOrientationSpinnerAdapter);
1121 mOrientationSpinner.setOnItemSelectedListener(itemSelectedListener);
1124 ArrayAdapter<SpinnerItem<Integer>> rangeOptionsSpinnerAdapter = new ArrayAdapter<>(
1125 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1126 mRangeOptionsSpinner = (Spinner) findViewById(R.id.range_options_spinner);
1127 mRangeOptionsSpinner.setAdapter(rangeOptionsSpinnerAdapter);
1128 mRangeOptionsSpinner.setOnItemSelectedListener(itemSelectedListener);
1129 updatePageRangeOptions(PrintDocumentInfo.PAGE_COUNT_UNKNOWN);
1132 mPageRangeTitle = (TextView) findViewById(R.id.page_range_title);
1133 mPageRangeEditText = (EditText) findViewById(R.id.page_range_edittext);
1134 mPageRangeEditText.setOnFocusChangeListener(mSelectAllOnFocusListener);
1135 mPageRangeEditText.addTextChangedListener(new RangeTextWatcher());
1137 // Advanced options button.
1138 mMoreOptionsButton = (Button) findViewById(R.id.more_options_button);
1139 mMoreOptionsButton.setOnClickListener(clickListener);
1142 mPrintButton = (ImageView) findViewById(R.id.print_button);
1143 mPrintButton.setOnClickListener(clickListener);
1146 private final class MyClickListener implements OnClickListener {
1148 public void onClick(View view) {
1149 if (view == mPrintButton) {
1150 if (mCurrentPrinter != null) {
1155 } else if (view == mMoreOptionsButton) {
1156 if (mCurrentPrinter != null) {
1157 startAdvancedPrintOptionsActivity(mCurrentPrinter);
1163 private static boolean canPrint(PrinterInfo printer) {
1164 return printer.getCapabilities() != null
1165 && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
1168 void updateOptionsUi() {
1169 // Always update the summary.
1172 if (mState == STATE_PRINT_CONFIRMED
1173 || mState == STATE_PRINT_COMPLETED
1174 || mState == STATE_PRINT_CANCELED
1175 || mState == STATE_UPDATE_FAILED
1176 || mState == STATE_CREATE_FILE_FAILED
1177 || mState == STATE_PRINTER_UNAVAILABLE
1178 || mState == STATE_UPDATE_SLOW) {
1179 if (mState != STATE_PRINTER_UNAVAILABLE) {
1180 mDestinationSpinner.setEnabled(false);
1182 mCopiesEditText.setEnabled(false);
1183 mCopiesEditText.setFocusable(false);
1184 mMediaSizeSpinner.setEnabled(false);
1185 mColorModeSpinner.setEnabled(false);
1186 mOrientationSpinner.setEnabled(false);
1187 mRangeOptionsSpinner.setEnabled(false);
1188 mPageRangeEditText.setEnabled(false);
1189 mPrintButton.setVisibility(View.GONE);
1190 mMoreOptionsButton.setEnabled(false);
1194 // If no current printer, or it has no capabilities, or it is not
1195 // available, we disable all print options except the destination.
1196 if (mCurrentPrinter == null || !canPrint(mCurrentPrinter)) {
1197 mCopiesEditText.setEnabled(false);
1198 mCopiesEditText.setFocusable(false);
1199 mMediaSizeSpinner.setEnabled(false);
1200 mColorModeSpinner.setEnabled(false);
1201 mOrientationSpinner.setEnabled(false);
1202 mRangeOptionsSpinner.setEnabled(false);
1203 mPageRangeEditText.setEnabled(false);
1204 mPrintButton.setVisibility(View.GONE);
1205 mMoreOptionsButton.setEnabled(false);
1209 PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
1210 PrintAttributes defaultAttributes = capabilities.getDefaults();
1213 mDestinationSpinner.setEnabled(true);
1216 mMediaSizeSpinner.setEnabled(true);
1218 List<MediaSize> mediaSizes = new ArrayList<>(capabilities.getMediaSizes());
1219 // Sort the media sizes based on the current locale.
1220 Collections.sort(mediaSizes, mMediaSizeComparator);
1222 PrintAttributes attributes = mPrintJob.getAttributes();
1224 // If the media sizes changed, we update the adapter and the spinner.
1225 boolean mediaSizesChanged = false;
1226 final int mediaSizeCount = mediaSizes.size();
1227 if (mediaSizeCount != mMediaSizeSpinnerAdapter.getCount()) {
1228 mediaSizesChanged = true;
1230 for (int i = 0; i < mediaSizeCount; i++) {
1231 if (!mediaSizes.get(i).equals(mMediaSizeSpinnerAdapter.getItem(i).value)) {
1232 mediaSizesChanged = true;
1237 if (mediaSizesChanged) {
1238 // Remember the old media size to try selecting it again.
1239 int oldMediaSizeNewIndex = AdapterView.INVALID_POSITION;
1240 MediaSize oldMediaSize = attributes.getMediaSize();
1242 // Rebuild the adapter data.
1243 mMediaSizeSpinnerAdapter.clear();
1244 for (int i = 0; i < mediaSizeCount; i++) {
1245 MediaSize mediaSize = mediaSizes.get(i);
1246 if (oldMediaSize != null
1247 && mediaSize.asPortrait().equals(oldMediaSize.asPortrait())) {
1248 // Update the index of the old selection.
1249 oldMediaSizeNewIndex = i;
1251 mMediaSizeSpinnerAdapter.add(new SpinnerItem<>(
1252 mediaSize, mediaSize.getLabel(getPackageManager())));
1255 if (oldMediaSizeNewIndex != AdapterView.INVALID_POSITION) {
1256 // Select the old media size - nothing really changed.
1257 if (mMediaSizeSpinner.getSelectedItemPosition() != oldMediaSizeNewIndex) {
1258 mMediaSizeSpinner.setSelection(oldMediaSizeNewIndex);
1261 // Select the first or the default.
1262 final int mediaSizeIndex = Math.max(mediaSizes.indexOf(
1263 defaultAttributes.getMediaSize()), 0);
1264 if (mMediaSizeSpinner.getSelectedItemPosition() != mediaSizeIndex) {
1265 mMediaSizeSpinner.setSelection(mediaSizeIndex);
1267 // Respect the orientation of the old selection.
1268 if (oldMediaSize != null) {
1269 if (oldMediaSize.isPortrait()) {
1270 attributes.setMediaSize(mMediaSizeSpinnerAdapter
1271 .getItem(mediaSizeIndex).value.asPortrait());
1273 attributes.setMediaSize(mMediaSizeSpinnerAdapter
1274 .getItem(mediaSizeIndex).value.asLandscape());
1281 mColorModeSpinner.setEnabled(true);
1282 final int colorModes = capabilities.getColorModes();
1284 // If the color modes changed, we update the adapter and the spinner.
1285 boolean colorModesChanged = false;
1286 if (Integer.bitCount(colorModes) != mColorModeSpinnerAdapter.getCount()) {
1287 colorModesChanged = true;
1289 int remainingColorModes = colorModes;
1290 int adapterIndex = 0;
1291 while (remainingColorModes != 0) {
1292 final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
1293 final int colorMode = 1 << colorBitOffset;
1294 remainingColorModes &= ~colorMode;
1295 if (colorMode != mColorModeSpinnerAdapter.getItem(adapterIndex).value) {
1296 colorModesChanged = true;
1302 if (colorModesChanged) {
1303 // Remember the old color mode to try selecting it again.
1304 int oldColorModeNewIndex = AdapterView.INVALID_POSITION;
1305 final int oldColorMode = attributes.getColorMode();
1307 // Rebuild the adapter data.
1308 mColorModeSpinnerAdapter.clear();
1309 String[] colorModeLabels = getResources().getStringArray(R.array.color_mode_labels);
1310 int remainingColorModes = colorModes;
1311 while (remainingColorModes != 0) {
1312 final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
1313 final int colorMode = 1 << colorBitOffset;
1314 if (colorMode == oldColorMode) {
1315 // Update the index of the old selection.
1316 oldColorModeNewIndex = colorBitOffset;
1318 remainingColorModes &= ~colorMode;
1319 mColorModeSpinnerAdapter.add(new SpinnerItem<>(colorMode,
1320 colorModeLabels[colorBitOffset]));
1322 if (oldColorModeNewIndex != AdapterView.INVALID_POSITION) {
1323 // Select the old color mode - nothing really changed.
1324 if (mColorModeSpinner.getSelectedItemPosition() != oldColorModeNewIndex) {
1325 mColorModeSpinner.setSelection(oldColorModeNewIndex);
1328 // Select the default.
1329 final int selectedColorMode = colorModes & defaultAttributes.getColorMode();
1330 final int itemCount = mColorModeSpinnerAdapter.getCount();
1331 for (int i = 0; i < itemCount; i++) {
1332 SpinnerItem<Integer> item = mColorModeSpinnerAdapter.getItem(i);
1333 if (selectedColorMode == item.value) {
1334 if (mColorModeSpinner.getSelectedItemPosition() != i) {
1335 mColorModeSpinner.setSelection(i);
1337 attributes.setColorMode(selectedColorMode);
1344 mOrientationSpinner.setEnabled(true);
1345 MediaSize mediaSize = attributes.getMediaSize();
1346 if (mediaSize != null) {
1347 if (mediaSize.isPortrait()
1348 && mOrientationSpinner.getSelectedItemPosition() != 0) {
1349 mOrientationSpinner.setSelection(0);
1350 } else if (!mediaSize.isPortrait()
1351 && mOrientationSpinner.getSelectedItemPosition() != 1) {
1352 mOrientationSpinner.setSelection(1);
1357 PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
1358 final int pageCount = getAdjustedPageCount(info);
1359 if (info != null && pageCount > 0) {
1360 if (pageCount == 1) {
1361 mRangeOptionsSpinner.setEnabled(false);
1363 mRangeOptionsSpinner.setEnabled(true);
1364 if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
1365 if (!mPageRangeEditText.isEnabled()) {
1366 mPageRangeEditText.setEnabled(true);
1367 mPageRangeEditText.setVisibility(View.VISIBLE);
1368 mPageRangeTitle.setVisibility(View.VISIBLE);
1369 mPageRangeEditText.requestFocus();
1370 InputMethodManager imm = (InputMethodManager)
1371 getSystemService(Context.INPUT_METHOD_SERVICE);
1372 imm.showSoftInput(mPageRangeEditText, 0);
1375 mPageRangeEditText.setEnabled(false);
1376 mPageRangeEditText.setVisibility(View.INVISIBLE);
1377 mPageRangeTitle.setVisibility(View.INVISIBLE);
1381 if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
1382 mRangeOptionsSpinner.setSelection(0);
1383 mPageRangeEditText.setText("");
1385 mRangeOptionsSpinner.setEnabled(false);
1386 mPageRangeEditText.setEnabled(false);
1387 mPageRangeEditText.setVisibility(View.INVISIBLE);
1388 mPageRangeTitle.setVisibility(View.INVISIBLE);
1391 final int newPageCount = getAdjustedPageCount(info);
1392 if (newPageCount != mCurrentPageCount) {
1393 mCurrentPageCount = newPageCount;
1394 updatePageRangeOptions(newPageCount);
1397 // Advanced print options
1398 ComponentName serviceName = mCurrentPrinter.getId().getServiceName();
1399 if (!TextUtils.isEmpty(PrintOptionUtils.getAdvancedOptionsActivityName(
1400 this, serviceName))) {
1401 mMoreOptionsButton.setVisibility(View.VISIBLE);
1402 mMoreOptionsButton.setEnabled(true);
1404 mMoreOptionsButton.setVisibility(View.GONE);
1405 mMoreOptionsButton.setEnabled(false);
1409 if (mDestinationSpinnerAdapter.getPdfPrinter() != mCurrentPrinter) {
1410 mPrintButton.setImageResource(com.android.internal.R.drawable.ic_print);
1411 mPrintButton.setContentDescription(getString(R.string.print_button));
1413 mPrintButton.setImageResource(R.drawable.ic_menu_savetopdf);
1414 mPrintButton.setContentDescription(getString(R.string.savetopdf_button));
1416 if (!mPrintedDocument.getDocumentInfo().laidout
1417 ||(mRangeOptionsSpinner.getSelectedItemPosition() == 1
1418 && (TextUtils.isEmpty(mPageRangeEditText.getText()) || hasErrors()))
1419 || (mRangeOptionsSpinner.getSelectedItemPosition() == 0
1420 && (mPrintedDocument.getDocumentInfo() == null || hasErrors()))) {
1421 mPrintButton.setVisibility(View.GONE);
1423 mPrintButton.setVisibility(View.VISIBLE);
1427 if (mDestinationSpinnerAdapter.getPdfPrinter() != mCurrentPrinter) {
1428 mCopiesEditText.setEnabled(true);
1429 mCopiesEditText.setFocusableInTouchMode(true);
1431 CharSequence text = mCopiesEditText.getText();
1432 if (TextUtils.isEmpty(text) || !MIN_COPIES_STRING.equals(text.toString())) {
1433 mCopiesEditText.setText(MIN_COPIES_STRING);
1435 mCopiesEditText.setEnabled(false);
1436 mCopiesEditText.setFocusable(false);
1438 if (mCopiesEditText.getError() == null
1439 && TextUtils.isEmpty(mCopiesEditText.getText())) {
1440 mCopiesEditText.setText(MIN_COPIES_STRING);
1441 mCopiesEditText.requestFocus();
1445 private void updateSummary() {
1446 CharSequence copiesText = null;
1447 CharSequence mediaSizeText = null;
1449 if (!TextUtils.isEmpty(mCopiesEditText.getText())) {
1450 copiesText = mCopiesEditText.getText();
1451 mSummaryCopies.setText(copiesText);
1454 final int selectedMediaIndex = mMediaSizeSpinner.getSelectedItemPosition();
1455 if (selectedMediaIndex >= 0) {
1456 SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(selectedMediaIndex);
1457 mediaSizeText = mediaItem.label;
1458 mSummaryPaperSize.setText(mediaSizeText);
1461 if (!TextUtils.isEmpty(copiesText) && !TextUtils.isEmpty(mediaSizeText)) {
1462 String summaryText = getString(R.string.summary_template, copiesText, mediaSizeText);
1463 mSummaryContainer.setContentDescription(summaryText);
1467 private void updatePageRangeOptions(int pageCount) {
1468 ArrayAdapter<SpinnerItem<Integer>> rangeOptionsSpinnerAdapter =
1469 (ArrayAdapter) mRangeOptionsSpinner.getAdapter();
1470 rangeOptionsSpinnerAdapter.clear();
1472 final int[] rangeOptionsValues = getResources().getIntArray(
1473 R.array.page_options_values);
1475 String pageCountLabel = (pageCount > 0) ? String.valueOf(pageCount) : "";
1476 String[] rangeOptionsLabels = new String[] {
1477 getString(R.string.template_all_pages, pageCountLabel),
1478 getString(R.string.template_page_range, pageCountLabel)
1481 final int rangeOptionsCount = rangeOptionsLabels.length;
1482 for (int i = 0; i < rangeOptionsCount; i++) {
1483 rangeOptionsSpinnerAdapter.add(new SpinnerItem<>(
1484 rangeOptionsValues[i], rangeOptionsLabels[i]));
1488 private PageRange[] computeSelectedPages() {
1493 if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
1494 List<PageRange> pageRanges = new ArrayList<>();
1495 mStringCommaSplitter.setString(mPageRangeEditText.getText().toString());
1497 while (mStringCommaSplitter.hasNext()) {
1498 String range = mStringCommaSplitter.next().trim();
1499 if (TextUtils.isEmpty(range)) {
1502 final int dashIndex = range.indexOf('-');
1503 final int fromIndex;
1506 if (dashIndex > 0) {
1507 fromIndex = Integer.parseInt(range.substring(0, dashIndex).trim()) - 1;
1508 // It is possible that the dash is at the end since the input
1509 // verification can has to allow the user to keep entering if
1510 // this would lead to a valid input. So we handle this.
1511 if (dashIndex < range.length() - 1) {
1512 String fromString = range.substring(dashIndex + 1, range.length()).trim();
1513 toIndex = Integer.parseInt(fromString) - 1;
1515 toIndex = fromIndex;
1518 fromIndex = toIndex = Integer.parseInt(range) - 1;
1521 PageRange pageRange = new PageRange(Math.min(fromIndex, toIndex),
1522 Math.max(fromIndex, toIndex));
1523 pageRanges.add(pageRange);
1526 PageRange[] pageRangesArray = new PageRange[pageRanges.size()];
1527 pageRanges.toArray(pageRangesArray);
1529 return PageRangeUtils.normalize(pageRangesArray);
1532 return ALL_PAGES_ARRAY;
1535 private int getAdjustedPageCount(PrintDocumentInfo info) {
1537 final int pageCount = info.getPageCount();
1538 if (pageCount != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) {
1542 // If the app does not tell us how many pages are in the
1543 // doc we ask for all pages and use the document page count.
1544 return mPrintPreviewController.getFilePageCount();
1547 private boolean hasErrors() {
1548 return (mCopiesEditText.getError() != null)
1549 || (mPageRangeEditText.getVisibility() == View.VISIBLE
1550 && mPageRangeEditText.getError() != null);
1553 public void onPrinterAvailable(PrinterInfo printer) {
1554 if (mCurrentPrinter.equals(printer)) {
1555 setState(STATE_CONFIGURING);
1556 if (canUpdateDocument()) {
1557 updateDocument(false);
1559 ensurePreviewUiShown();
1564 public void onPrinterUnavailable(PrinterInfo printer) {
1565 if (mCurrentPrinter.getId().equals(printer.getId())) {
1566 setState(STATE_PRINTER_UNAVAILABLE);
1567 if (mPrintedDocument.isUpdating()) {
1568 mPrintedDocument.cancel();
1570 ensureErrorUiShown(getString(R.string.print_error_printer_unavailable),
1571 PrintErrorFragment.ACTION_NONE);
1576 private boolean canUpdateDocument() {
1577 if (mPrintedDocument.isDestroyed()) {
1585 PrintAttributes attributes = mPrintJob.getAttributes();
1587 final int colorMode = attributes.getColorMode();
1588 if (colorMode != PrintAttributes.COLOR_MODE_COLOR
1589 && colorMode != PrintAttributes.COLOR_MODE_MONOCHROME) {
1592 if (attributes.getMediaSize() == null) {
1595 if (attributes.getMinMargins() == null) {
1598 if (attributes.getResolution() == null) {
1602 if (mCurrentPrinter == null) {
1605 PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
1606 if (capabilities == null) {
1609 if (mCurrentPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE) {
1616 private void transformDocumentAndFinish(final Uri writeToUri) {
1617 // If saving to PDF, apply the attibutes as we are acting as a print service.
1618 PrintAttributes attributes = mDestinationSpinnerAdapter.getPdfPrinter() == mCurrentPrinter
1619 ? mPrintJob.getAttributes() : null;
1620 new DocumentTransformer(this, mPrintJob, mFileProvider, attributes, new Runnable() {
1623 if (writeToUri != null) {
1624 mPrintedDocument.writeContent(getContentResolver(), writeToUri);
1631 private void doFinish() {
1632 if (mState != STATE_INITIALIZING) {
1633 mProgressMessageController.cancel();
1634 mPrinterRegistry.setTrackedPrinter(null);
1635 mSpoolerProvider.destroy();
1636 mPrintedDocument.finish();
1637 mPrintedDocument.destroy();
1638 mPrintPreviewController.destroy(new Runnable() {
1649 private final class SpinnerItem<T> {
1651 final CharSequence label;
1653 public SpinnerItem(T value, CharSequence label) {
1658 public String toString() {
1659 return label.toString();
1663 private final class PrinterAvailabilityDetector implements Runnable {
1664 private static final long UNAVAILABLE_TIMEOUT_MILLIS = 10000; // 10sec
1666 private boolean mPosted;
1668 private boolean mPrinterUnavailable;
1670 private PrinterInfo mPrinter;
1672 public void updatePrinter(PrinterInfo printer) {
1673 if (printer.equals(mDestinationSpinnerAdapter.getPdfPrinter())) {
1677 final boolean available = printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE
1678 && printer.getCapabilities() != null;
1679 final boolean notifyIfAvailable;
1681 if (mPrinter == null || !mPrinter.getId().equals(printer.getId())) {
1682 notifyIfAvailable = true;
1684 mPrinterUnavailable = false;
1685 mPrinter = new PrinterInfo.Builder(printer).build();
1688 (mPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE
1689 && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE)
1690 || (mPrinter.getCapabilities() == null
1691 && printer.getCapabilities() != null);
1692 mPrinter.copyFrom(printer);
1697 mPrinterUnavailable = false;
1698 if (notifyIfAvailable) {
1699 onPrinterAvailable(mPrinter);
1702 if (!mPrinterUnavailable) {
1708 public void cancel() {
1710 mPrinterUnavailable = false;
1713 private void postIfNeeded() {
1716 mDestinationSpinner.postDelayed(this, UNAVAILABLE_TIMEOUT_MILLIS);
1720 private void unpostIfNeeded() {
1723 mDestinationSpinner.removeCallbacks(this);
1730 mPrinterUnavailable = true;
1731 onPrinterUnavailable(mPrinter);
1735 private static final class PrinterHolder {
1736 PrinterInfo printer;
1739 public PrinterHolder(PrinterInfo printer) {
1740 this.printer = printer;
1744 private final class DestinationAdapter extends BaseAdapter
1745 implements PrinterRegistry.OnPrintersChangeListener {
1746 private final List<PrinterHolder> mPrinterHolders = new ArrayList<>();
1748 private final PrinterHolder mFakePdfPrinterHolder;
1750 private boolean mHistoricalPrintersLoaded;
1752 public DestinationAdapter() {
1753 mHistoricalPrintersLoaded = mPrinterRegistry.areHistoricalPrintersLoaded();
1754 if (mHistoricalPrintersLoaded) {
1755 addPrinters(mPrinterHolders, mPrinterRegistry.getPrinters());
1757 mPrinterRegistry.setOnPrintersChangeListener(this);
1758 mFakePdfPrinterHolder = new PrinterHolder(createFakePdfPrinter());
1761 public PrinterInfo getPdfPrinter() {
1762 return mFakePdfPrinterHolder.printer;
1765 public int getPrinterIndex(PrinterId printerId) {
1766 for (int i = 0; i < getCount(); i++) {
1767 PrinterHolder printerHolder = (PrinterHolder) getItem(i);
1768 if (printerHolder != null && !printerHolder.removed
1769 && printerHolder.printer.getId().equals(printerId)) {
1773 return AdapterView.INVALID_POSITION;
1776 public void ensurePrinterInVisibleAdapterPosition(PrinterId printerId) {
1777 final int printerCount = mPrinterHolders.size();
1778 for (int i = 0; i < printerCount; i++) {
1779 PrinterHolder printerHolder = mPrinterHolders.get(i);
1780 if (printerHolder.printer.getId().equals(printerId)) {
1781 // If already in the list - do nothing.
1782 if (i < getCount() - 2) {
1785 // Else replace the last one (two items are not printers).
1786 final int lastPrinterIndex = getCount() - 3;
1787 mPrinterHolders.set(i, mPrinterHolders.get(lastPrinterIndex));
1788 mPrinterHolders.set(lastPrinterIndex, printerHolder);
1789 notifyDataSetChanged();
1796 public int getCount() {
1797 if (mHistoricalPrintersLoaded) {
1798 return Math.min(mPrinterHolders.size() + 2, DEST_ADAPTER_MAX_ITEM_COUNT);
1804 public boolean isEnabled(int position) {
1805 Object item = getItem(position);
1806 if (item instanceof PrinterHolder) {
1807 PrinterHolder printerHolder = (PrinterHolder) item;
1808 return !printerHolder.removed
1809 && printerHolder.printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
1815 public Object getItem(int position) {
1816 if (mPrinterHolders.isEmpty()) {
1817 if (position == 0) {
1818 return mFakePdfPrinterHolder;
1822 return mPrinterHolders.get(position);
1824 if (position == 1) {
1825 return mFakePdfPrinterHolder;
1827 if (position < getCount() - 1) {
1828 return mPrinterHolders.get(position - 1);
1835 public long getItemId(int position) {
1836 if (mPrinterHolders.isEmpty()) {
1837 if (position == 0) {
1838 return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
1839 } else if (position == 1) {
1840 return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS;
1843 if (position == 1) {
1844 return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
1846 if (position == getCount() - 1) {
1847 return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS;
1854 public View getDropDownView(int position, View convertView, ViewGroup parent) {
1855 View view = getView(position, convertView, parent);
1856 view.setEnabled(isEnabled(position));
1861 public View getView(int position, View convertView, ViewGroup parent) {
1862 if (convertView == null) {
1863 convertView = getLayoutInflater().inflate(
1864 R.layout.printer_dropdown_item, parent, false);
1867 CharSequence title = null;
1868 CharSequence subtitle = null;
1869 Drawable icon = null;
1871 if (mPrinterHolders.isEmpty()) {
1872 if (position == 0 && getPdfPrinter() != null) {
1873 PrinterHolder printerHolder = (PrinterHolder) getItem(position);
1874 title = printerHolder.printer.getName();
1875 icon = getResources().getDrawable(R.drawable.ic_menu_savetopdf);
1876 } else if (position == 1) {
1877 title = getString(R.string.all_printers);
1880 if (position == 1 && getPdfPrinter() != null) {
1881 PrinterHolder printerHolder = (PrinterHolder) getItem(position);
1882 title = printerHolder.printer.getName();
1883 icon = getResources().getDrawable(R.drawable.ic_menu_savetopdf);
1884 } else if (position == getCount() - 1) {
1885 title = getString(R.string.all_printers);
1887 PrinterHolder printerHolder = (PrinterHolder) getItem(position);
1888 title = printerHolder.printer.getName();
1890 PackageInfo packageInfo = getPackageManager().getPackageInfo(
1891 printerHolder.printer.getId().getServiceName().getPackageName(), 0);
1892 subtitle = packageInfo.applicationInfo.loadLabel(getPackageManager());
1893 icon = packageInfo.applicationInfo.loadIcon(getPackageManager());
1894 } catch (NameNotFoundException nnfe) {
1900 TextView titleView = (TextView) convertView.findViewById(R.id.title);
1901 titleView.setText(title);
1903 TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle);
1904 if (!TextUtils.isEmpty(subtitle)) {
1905 subtitleView.setText(subtitle);
1906 subtitleView.setVisibility(View.VISIBLE);
1908 subtitleView.setText(null);
1909 subtitleView.setVisibility(View.GONE);
1912 ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
1914 iconView.setImageDrawable(icon);
1915 iconView.setVisibility(View.VISIBLE);
1917 iconView.setVisibility(View.INVISIBLE);
1924 public void onPrintersChanged(List<PrinterInfo> printers) {
1925 // We rearrange the printers if the user selects a printer
1926 // not shown in the initial short list. Therefore, we have
1927 // to keep the printer order.
1929 // Check if historical printers are loaded as this adapter is open
1930 // for busyness only if they are. This member is updated here and
1931 // when the adapter is created because the historical printers may
1932 // be loaded before or after the adapter is created.
1933 mHistoricalPrintersLoaded = mPrinterRegistry.areHistoricalPrintersLoaded();
1935 // No old printers - do not bother keeping their position.
1936 if (mPrinterHolders.isEmpty()) {
1937 addPrinters(mPrinterHolders, printers);
1938 notifyDataSetChanged();
1942 // Add the new printers to a map.
1943 ArrayMap<PrinterId, PrinterInfo> newPrintersMap = new ArrayMap<>();
1944 final int printerCount = printers.size();
1945 for (int i = 0; i < printerCount; i++) {
1946 PrinterInfo printer = printers.get(i);
1947 newPrintersMap.put(printer.getId(), printer);
1950 List<PrinterHolder> newPrinterHolders = new ArrayList<>();
1952 // Update printers we already have which are either updated or removed.
1953 // We do not remove printers if the currently selected printer is removed
1954 // to prevent the user printing to a wrong printer.
1955 final int oldPrinterCount = mPrinterHolders.size();
1956 for (int i = 0; i < oldPrinterCount; i++) {
1957 PrinterHolder printerHolder = mPrinterHolders.get(i);
1958 PrinterId oldPrinterId = printerHolder.printer.getId();
1959 PrinterInfo updatedPrinter = newPrintersMap.remove(oldPrinterId);
1960 if (updatedPrinter != null) {
1961 printerHolder.printer = updatedPrinter;
1963 printerHolder.removed = true;
1965 newPrinterHolders.add(printerHolder);
1968 // Add the rest of the new printers, i.e. what is left.
1969 addPrinters(newPrinterHolders, newPrintersMap.values());
1971 mPrinterHolders.clear();
1972 mPrinterHolders.addAll(newPrinterHolders);
1974 notifyDataSetChanged();
1978 public void onPrintersInvalid() {
1979 mPrinterHolders.clear();
1980 notifyDataSetInvalidated();
1983 public PrinterHolder getPrinterHolder(PrinterId printerId) {
1984 final int itemCount = getCount();
1985 for (int i = 0; i < itemCount; i++) {
1986 Object item = getItem(i);
1987 if (item instanceof PrinterHolder) {
1988 PrinterHolder printerHolder = (PrinterHolder) item;
1989 if (printerId.equals(printerHolder.printer.getId())) {
1990 return printerHolder;
1997 public void pruneRemovedPrinters() {
1998 final int holderCounts = mPrinterHolders.size();
1999 for (int i = holderCounts - 1; i >= 0; i--) {
2000 PrinterHolder printerHolder = mPrinterHolders.get(i);
2001 if (printerHolder.removed) {
2002 mPrinterHolders.remove(i);
2007 private void addPrinters(List<PrinterHolder> list, Collection<PrinterInfo> printers) {
2008 for (PrinterInfo printer : printers) {
2009 PrinterHolder printerHolder = new PrinterHolder(printer);
2010 list.add(printerHolder);
2014 private PrinterInfo createFakePdfPrinter() {
2015 MediaSize defaultMediaSize = MediaSizeUtils.getDefault(PrintActivity.this);
2017 PrinterId printerId = new PrinterId(getComponentName(), "PDF printer");
2019 PrinterCapabilitiesInfo.Builder builder =
2020 new PrinterCapabilitiesInfo.Builder(printerId);
2022 String[] mediaSizeIds = getResources().getStringArray(R.array.pdf_printer_media_sizes);
2023 final int mediaSizeIdCount = mediaSizeIds.length;
2024 for (int i = 0; i < mediaSizeIdCount; i++) {
2025 String id = mediaSizeIds[i];
2026 MediaSize mediaSize = MediaSize.getStandardMediaSizeById(id);
2027 builder.addMediaSize(mediaSize, mediaSize.equals(defaultMediaSize));
2030 builder.addResolution(new Resolution("PDF resolution", "PDF resolution", 300, 300),
2032 builder.setColorModes(PrintAttributes.COLOR_MODE_COLOR
2033 | PrintAttributes.COLOR_MODE_MONOCHROME, PrintAttributes.COLOR_MODE_COLOR);
2035 return new PrinterInfo.Builder(printerId, getString(R.string.save_as_pdf),
2036 PrinterInfo.STATUS_IDLE).setCapabilities(builder.build()).build();
2040 private final class PrintersObserver extends DataSetObserver {
2042 public void onChanged() {
2043 PrinterInfo oldPrinterState = mCurrentPrinter;
2044 if (oldPrinterState == null) {
2048 PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
2049 oldPrinterState.getId());
2050 if (printerHolder == null) {
2053 PrinterInfo newPrinterState = printerHolder.printer;
2055 if (!printerHolder.removed) {
2056 mDestinationSpinnerAdapter.pruneRemovedPrinters();
2058 onPrinterUnavailable(newPrinterState);
2061 if (oldPrinterState.equals(newPrinterState)) {
2065 PrinterCapabilitiesInfo oldCapab = oldPrinterState.getCapabilities();
2066 PrinterCapabilitiesInfo newCapab = newPrinterState.getCapabilities();
2068 final boolean hasCapab = newCapab != null;
2069 final boolean gotCapab = oldCapab == null && newCapab != null;
2070 final boolean lostCapab = oldCapab != null && newCapab == null;
2071 final boolean capabChanged = capabilitiesChanged(oldCapab, newCapab);
2073 final int oldStatus = oldPrinterState.getStatus();
2074 final int newStatus = newPrinterState.getStatus();
2076 final boolean isActive = newStatus != PrinterInfo.STATUS_UNAVAILABLE;
2077 final boolean becameActive = (oldStatus == PrinterInfo.STATUS_UNAVAILABLE
2078 && oldStatus != newStatus);
2079 final boolean becameInactive = (newStatus == PrinterInfo.STATUS_UNAVAILABLE
2080 && oldStatus != newStatus);
2082 mPrinterAvailabilityDetector.updatePrinter(newPrinterState);
2084 oldPrinterState.copyFrom(newPrinterState);
2086 if ((isActive && gotCapab) || (becameActive && hasCapab)) {
2087 if (hasCapab && capabChanged) {
2088 updatePrintAttributesFromCapabilities(newCapab);
2089 updatePrintPreviewController(false);
2091 onPrinterAvailable(newPrinterState);
2092 } else if ((becameInactive && hasCapab) || (isActive && lostCapab)) {
2093 onPrinterUnavailable(newPrinterState);
2096 final boolean updateNeeded = ((capabChanged && hasCapab && isActive)
2097 || (becameActive && hasCapab) || (isActive && gotCapab));
2099 if (updateNeeded && canUpdateDocument()) {
2100 updateDocument(false);
2106 private boolean capabilitiesChanged(PrinterCapabilitiesInfo oldCapabilities,
2107 PrinterCapabilitiesInfo newCapabilities) {
2108 if (oldCapabilities == null) {
2109 if (newCapabilities != null) {
2112 } else if (!oldCapabilities.equals(newCapabilities)) {
2119 private final class MyOnItemSelectedListener implements AdapterView.OnItemSelectedListener {
2121 public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) {
2122 if (spinner == mDestinationSpinner) {
2123 if (position == AdapterView.INVALID_POSITION) {
2127 if (id == DEST_ADAPTER_ITEM_ID_ALL_PRINTERS) {
2128 startSelectPrinterActivity();
2132 PrinterHolder currentItem = (PrinterHolder) mDestinationSpinner.getSelectedItem();
2133 PrinterInfo currentPrinter = (currentItem != null) ? currentItem.printer : null;
2135 // Why on earth item selected is called if no selection changed.
2136 if (mCurrentPrinter == currentPrinter) {
2140 mCurrentPrinter = currentPrinter;
2142 PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
2143 currentPrinter.getId());
2144 if (!printerHolder.removed) {
2145 setState(STATE_CONFIGURING);
2146 mDestinationSpinnerAdapter.pruneRemovedPrinters();
2147 ensurePreviewUiShown();
2150 mPrintJob.setPrinterId(currentPrinter.getId());
2151 mPrintJob.setPrinterName(currentPrinter.getName());
2153 mPrinterRegistry.setTrackedPrinter(currentPrinter.getId());
2155 PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities();
2156 if (capabilities != null) {
2157 updatePrintAttributesFromCapabilities(capabilities);
2160 mPrinterAvailabilityDetector.updatePrinter(currentPrinter);
2161 } else if (spinner == mMediaSizeSpinner) {
2162 SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position);
2163 PrintAttributes attributes = mPrintJob.getAttributes();
2164 if (mOrientationSpinner.getSelectedItemPosition() == 0) {
2165 attributes.setMediaSize(mediaItem.value.asPortrait());
2167 attributes.setMediaSize(mediaItem.value.asLandscape());
2169 } else if (spinner == mColorModeSpinner) {
2170 SpinnerItem<Integer> colorModeItem = mColorModeSpinnerAdapter.getItem(position);
2171 mPrintJob.getAttributes().setColorMode(colorModeItem.value);
2172 } else if (spinner == mOrientationSpinner) {
2173 SpinnerItem<Integer> orientationItem = mOrientationSpinnerAdapter.getItem(position);
2174 PrintAttributes attributes = mPrintJob.getAttributes();
2175 if (mMediaSizeSpinner.getSelectedItem() != null) {
2176 if (orientationItem.value == ORIENTATION_PORTRAIT) {
2177 attributes.copyFrom(attributes.asPortrait());
2179 attributes.copyFrom(attributes.asLandscape());
2182 } else if (spinner == mRangeOptionsSpinner) {
2183 if (mRangeOptionsSpinner.getSelectedItemPosition() == 0) {
2184 mPageRangeEditText.setText("");
2185 } else if (TextUtils.isEmpty(mPageRangeEditText.getText())) {
2186 mPageRangeEditText.setError("");
2190 if (canUpdateDocument()) {
2191 updateDocument(false);
2198 public void onNothingSelected(AdapterView<?> parent) {
2203 private final class SelectAllOnFocusListener implements OnFocusChangeListener {
2205 public void onFocusChange(View view, boolean hasFocus) {
2206 EditText editText = (EditText) view;
2207 if (!TextUtils.isEmpty(editText.getText())) {
2208 editText.setSelection(editText.getText().length());
2213 private final class RangeTextWatcher implements TextWatcher {
2215 public void onTextChanged(CharSequence s, int start, int before, int count) {
2220 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2225 public void afterTextChanged(Editable editable) {
2226 final boolean hadErrors = hasErrors();
2228 String text = editable.toString();
2230 if (TextUtils.isEmpty(text)) {
2231 mPageRangeEditText.setError("");
2236 String escapedText = PATTERN_ESCAPE_SPECIAL_CHARS.matcher(text).replaceAll("////");
2237 if (!PATTERN_PAGE_RANGE.matcher(escapedText).matches()) {
2238 mPageRangeEditText.setError("");
2243 PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
2244 final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
2247 Matcher matcher = PATTERN_DIGITS.matcher(text);
2248 while (matcher.find()) {
2249 String numericString = text.substring(matcher.start(), matcher.end()).trim();
2250 if (TextUtils.isEmpty(numericString)) {
2253 final int pageIndex = Integer.parseInt(numericString);
2254 if (pageIndex < 1 || pageIndex > pageCount) {
2255 mPageRangeEditText.setError("");
2261 // We intentionally do not catch the case of the from page being
2262 // greater than the to page. When computing the requested pages
2263 // we just swap them if necessary.
2265 mPageRangeEditText.setError(null);
2266 mPrintButton.setEnabled(true);
2269 if (hadErrors && !hasErrors()) {
2275 private final class EditTextWatcher implements TextWatcher {
2277 public void onTextChanged(CharSequence s, int start, int before, int count) {
2282 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2287 public void afterTextChanged(Editable editable) {
2288 final boolean hadErrors = hasErrors();
2290 if (editable.length() == 0) {
2291 mCopiesEditText.setError("");
2298 copies = Integer.parseInt(editable.toString());
2299 } catch (NumberFormatException nfe) {
2303 if (copies < MIN_COPIES) {
2304 mCopiesEditText.setError("");
2309 mPrintJob.setCopies(copies);
2311 mCopiesEditText.setError(null);
2315 if (hadErrors && canUpdateDocument()) {
2316 updateDocument(false);
2321 private final class ProgressMessageController implements Runnable {
2322 private static final long PROGRESS_TIMEOUT_MILLIS = 1000;
2324 private final Handler mHandler;
2326 private boolean mPosted;
2328 public ProgressMessageController(Context context) {
2329 mHandler = new Handler(context.getMainLooper(), null, false);
2332 public void post() {
2337 mHandler.postDelayed(this, PROGRESS_TIMEOUT_MILLIS);
2340 public void cancel() {
2345 mHandler.removeCallbacks(this);
2351 setState(STATE_UPDATE_SLOW);
2352 ensureProgressUiShown();
2357 private static final class DocumentTransformer implements ServiceConnection {
2358 private static final String TEMP_FILE_PREFIX = "print_job";
2359 private static final String TEMP_FILE_EXTENSION = ".pdf";
2361 private final Context mContext;
2363 private final MutexFileProvider mFileProvider;
2365 private final PrintJobInfo mPrintJob;
2367 private final PageRange[] mPagesToShred;
2369 private final PrintAttributes mAttributesToApply;
2371 private final Runnable mCallback;
2373 public DocumentTransformer(Context context, PrintJobInfo printJob,
2374 MutexFileProvider fileProvider, PrintAttributes attributes,
2375 Runnable callback) {
2377 mPrintJob = printJob;
2378 mFileProvider = fileProvider;
2379 mCallback = callback;
2380 mPagesToShred = computePagesToShred(mPrintJob);
2381 mAttributesToApply = attributes;
2384 public void transform() {
2385 // If we have only the pages we want, done.
2386 if (mPagesToShred.length <= 0 && mAttributesToApply == null) {
2391 // Bind to the manipulation service and the work
2392 // will be performed upon connection to the service.
2393 Intent intent = new Intent(PdfManipulationService.ACTION_GET_EDITOR);
2394 intent.setClass(mContext, PdfManipulationService.class);
2395 mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
2399 public void onServiceConnected(ComponentName name, IBinder service) {
2400 final IPdfEditor editor = IPdfEditor.Stub.asInterface(service);
2401 new AsyncTask<Void, Void, Void>() {
2403 protected Void doInBackground(Void... params) {
2404 // It's OK to access the data members as they are
2405 // final and this code is the last one to touch
2406 // them as shredding is the very last step, so the
2407 // UI is not interactive at this point.
2408 doTransform(editor);
2414 protected void onPostExecute(Void aVoid) {
2415 mContext.unbindService(DocumentTransformer.this);
2418 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
2422 public void onServiceDisconnected(ComponentName name) {
2426 private void doTransform(IPdfEditor editor) {
2427 File tempFile = null;
2428 ParcelFileDescriptor src = null;
2429 ParcelFileDescriptor dst = null;
2430 InputStream in = null;
2431 OutputStream out = null;
2433 File jobFile = mFileProvider.acquireFile(null);
2434 src = ParcelFileDescriptor.open(jobFile, ParcelFileDescriptor.MODE_READ_WRITE);
2436 // Open the document.
2437 editor.openDocument(src);
2439 // We passed the fd over IPC, close this one.
2443 editor.removePages(mPagesToShred);
2445 // Apply print attributes if needed.
2446 if (mAttributesToApply != null) {
2447 editor.applyPrintAttributes(mAttributesToApply);
2450 // Write the modified PDF to a temp file.
2451 tempFile = File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_EXTENSION,
2452 mContext.getCacheDir());
2453 dst = ParcelFileDescriptor.open(tempFile, ParcelFileDescriptor.MODE_READ_WRITE);
2457 // Close the document.
2458 editor.closeDocument();
2460 // Copy the temp file over the print job file.
2462 in = new FileInputStream(tempFile);
2463 out = new FileOutputStream(jobFile);
2464 Streams.copy(in, out);
2465 } catch (IOException|RemoteException e) {
2466 Log.e(LOG_TAG, "Error dropping pages", e);
2468 IoUtils.closeQuietly(src);
2469 IoUtils.closeQuietly(dst);
2470 IoUtils.closeQuietly(in);
2471 IoUtils.closeQuietly(out);
2472 if (tempFile != null) {
2475 mFileProvider.releaseFile();
2479 private void updatePrintJob() {
2480 // Update the print job pages.
2481 final int newPageCount = PageRangeUtils.getNormalizedPageCount(
2482 mPrintJob.getPages(), 0);
2483 mPrintJob.setPages(new PageRange[]{PageRange.ALL_PAGES});
2485 // Update the print job document info.
2486 PrintDocumentInfo oldDocInfo = mPrintJob.getDocumentInfo();
2487 PrintDocumentInfo newDocInfo = new PrintDocumentInfo
2488 .Builder(oldDocInfo.getName())
2489 .setContentType(oldDocInfo.getContentType())
2490 .setPageCount(newPageCount)
2492 mPrintJob.setDocumentInfo(newDocInfo);
2495 private static PageRange[] computePagesToShred(PrintJobInfo printJob) {
2496 List<PageRange> rangesToShred = new ArrayList<>();
2497 PageRange previousRange = null;
2499 final int pageCount = printJob.getDocumentInfo().getPageCount();
2501 PageRange[] printedPages = printJob.getPages();
2502 final int rangeCount = printedPages.length;
2503 for (int i = 0; i < rangeCount; i++) {
2504 PageRange range = PageRangeUtils.asAbsoluteRange(printedPages[i], pageCount);
2506 if (previousRange == null) {
2507 final int startPageIdx = 0;
2508 final int endPageIdx = range.getStart() - 1;
2509 if (startPageIdx <= endPageIdx) {
2510 PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
2511 rangesToShred.add(removedRange);
2514 final int startPageIdx = previousRange.getEnd() + 1;
2515 final int endPageIdx = range.getStart() - 1;
2516 if (startPageIdx <= endPageIdx) {
2517 PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
2518 rangesToShred.add(removedRange);
2522 if (i == rangeCount - 1) {
2523 final int startPageIdx = range.getEnd() + 1;
2524 final int endPageIdx = printJob.getDocumentInfo().getPageCount() - 1;
2525 if (startPageIdx <= endPageIdx) {
2526 PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
2527 rangesToShred.add(removedRange);
2531 previousRange = range;
2534 PageRange[] result = new PageRange[rangesToShred.size()];
2535 rangesToShred.toArray(result);