OSDN Git Service

Crash in print spooler when back pressed before content update complted.
[android-x86/frameworks-base.git] / packages / PrintSpooler / src / com / android / printspooler / ui / PrintActivity.java
1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package com.android.printspooler.ui;
18
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;
74
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;
92
93 import java.io.File;
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;
106
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";
111
112     private static final boolean DEBUG = false;
113
114     public static final String INTENT_EXTRA_PRINTER_ID = "INTENT_EXTRA_PRINTER_ID";
115
116     private static final String FRAGMENT_TAG = "FRAGMENT_TAG";
117
118     private static final int ORIENTATION_PORTRAIT = 0;
119     private static final int ORIENTATION_LANDSCAPE = 1;
120
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;
124
125     private static final int DEST_ADAPTER_MAX_ITEM_COUNT = 9;
126
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;
129
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;
139
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;
143
144     private static final int MIN_COPIES = 1;
145     private static final String MIN_COPIES_STRING = String.valueOf(MIN_COPIES);
146
147     private static final Pattern PATTERN_DIGITS = Pattern.compile("[\\d]+");
148
149     private static final Pattern PATTERN_ESCAPE_SPECIAL_CHARS = Pattern.compile(
150             "(?=[]\\[+&|!(){}^\"~*?:\\\\])");
151
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]*)+");
155
156     public static final PageRange[] ALL_PAGES_ARRAY = new PageRange[]{PageRange.ALL_PAGES};
157
158     private final PrinterAvailabilityDetector mPrinterAvailabilityDetector =
159             new PrinterAvailabilityDetector();
160
161     private final SimpleStringSplitter mStringCommaSplitter = new SimpleStringSplitter(',');
162
163     private final OnFocusChangeListener mSelectAllOnFocusListener = new SelectAllOnFocusListener();
164
165     private PrintSpoolerProvider mSpoolerProvider;
166
167     private PrintPreviewController mPrintPreviewController;
168
169     private PrintJobInfo mPrintJob;
170     private RemotePrintDocument mPrintedDocument;
171     private PrinterRegistry mPrinterRegistry;
172
173     private EditText mCopiesEditText;
174
175     private TextView mPageRangeTitle;
176     private EditText mPageRangeEditText;
177
178     private Spinner mDestinationSpinner;
179     private DestinationAdapter mDestinationSpinnerAdapter;
180
181     private Spinner mMediaSizeSpinner;
182     private ArrayAdapter<SpinnerItem<MediaSize>> mMediaSizeSpinnerAdapter;
183
184     private Spinner mColorModeSpinner;
185     private ArrayAdapter<SpinnerItem<Integer>> mColorModeSpinnerAdapter;
186
187     private Spinner mOrientationSpinner;
188     private ArrayAdapter<SpinnerItem<Integer>> mOrientationSpinnerAdapter;
189
190     private Spinner mRangeOptionsSpinner;
191
192     private PrintContentView mOptionsContent;
193
194     private View mSummaryContainer;
195     private TextView mSummaryCopies;
196     private TextView mSummaryPaperSize;
197
198     private Button mMoreOptionsButton;
199
200     private ImageView mPrintButton;
201
202     private ProgressMessageController mProgressMessageController;
203     private MutexFileProvider mFileProvider;
204
205     private MediaSizeComparator mMediaSizeComparator;
206
207     private PrinterInfo mCurrentPrinter;
208
209     private PageRange[] mSelectedPages;
210
211     private String mCallingPackageName;
212
213     private int mCurrentPageCount;
214
215     private int mState = STATE_INITIALIZING;
216
217     private int mUiState = UI_STATE_PREVIEW;
218
219     @Override
220     public void onCreate(Bundle savedInstanceState) {
221         super.onCreate(savedInstanceState);
222
223         Bundle extras = getIntent().getExtras();
224
225         mPrintJob = extras.getParcelable(PrintManager.EXTRA_PRINT_JOB);
226         if (mPrintJob == null) {
227             throw new IllegalArgumentException(PrintManager.EXTRA_PRINT_JOB
228                     + " cannot be null");
229         }
230         mPrintJob.setAttributes(new PrintAttributes.Builder().build());
231
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");
236         }
237
238         mCallingPackageName = extras.getString(DocumentsContract.EXTRA_PACKAGE_NAME);
239
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,
243                 new Runnable() {
244             @Override
245             public void run() {
246                 onConnectedToPrintSpooler(adapter);
247             }
248         });
249     }
250
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,
258                 new Runnable() {
259             @Override
260             public void run() {
261                 onPrinterRegistryReady(documentAdapter);
262             }
263         });
264     }
265
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);
272
273         try {
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);
280         }
281
282         mPrintPreviewController = new PrintPreviewController(PrintActivity.this,
283                 mFileProvider);
284         mPrintedDocument = new RemotePrintDocument(PrintActivity.this,
285                 IPrintDocumentAdapter.Stub.asInterface(documentAdapter),
286                 mFileProvider, new RemotePrintDocument.RemoteAdapterDeathObserver() {
287             @Override
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())) {
292                     return;
293                 }
294                 if (mPrintedDocument.isUpdating()) {
295                     mPrintedDocument.cancel();
296                 }
297                 setState(STATE_PRINT_CANCELED);
298                 doFinish();
299             }
300         }, PrintActivity.this);
301         mProgressMessageController = new ProgressMessageController(
302                 PrintActivity.this);
303         mMediaSizeComparator = new MediaSizeComparator(PrintActivity.this);
304         mDestinationSpinnerAdapter = new DestinationAdapter();
305
306         bindUi();
307         updateOptionsUi();
308
309         // Now show the updated UI to avoid flicker.
310         mOptionsContent.setVisibility(View.VISIBLE);
311         mSelectedPages = computeSelectedPages();
312         mPrintedDocument.start();
313
314         ensurePreviewUiShown();
315
316         setState(STATE_CONFIGURING);
317     }
318
319     @Override
320     public void onResume() {
321         super.onResume();
322         if (mState != STATE_INITIALIZING && mCurrentPrinter != null) {
323             mPrinterRegistry.setTrackedPrinter(mCurrentPrinter.getId());
324         }
325     }
326
327     @Override
328     public void onPause() {
329         PrintSpoolerService spooler = mSpoolerProvider.getSpooler();
330
331         if (mState == STATE_INITIALIZING) {
332             if (isFinishing()) {
333                 spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_CANCELED, null);
334             }
335             super.onPause();
336             return;
337         }
338
339         if (isFinishing()) {
340             spooler.updatePrintJobUserConfigurableOptionsNoPersistence(mPrintJob);
341
342             switch (mState) {
343                 case STATE_PRINT_CONFIRMED: {
344                     spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_QUEUED, null);
345                 } break;
346
347                 case STATE_PRINT_COMPLETED: {
348                     spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_COMPLETED, null);
349                 } break;
350
351                 case STATE_CREATE_FILE_FAILED: {
352                     spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_FAILED,
353                             getString(R.string.print_write_error_message));
354                 } break;
355
356                 default: {
357                     spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_CANCELED, null);
358                 } break;
359             }
360         }
361
362         mPrinterAvailabilityDetector.cancel();
363         mPrinterRegistry.setTrackedPrinter(null);
364
365         super.onPause();
366     }
367
368     @Override
369     public boolean onKeyDown(int keyCode, KeyEvent event) {
370         if (keyCode == KeyEvent.KEYCODE_BACK) {
371             event.startTracking();
372             return true;
373         }
374         return super.onKeyDown(keyCode, event);
375     }
376
377     @Override
378     public boolean onKeyUp(int keyCode, KeyEvent event) {
379         if (mState == STATE_INITIALIZING) {
380             doFinish();
381             return true;
382         }
383
384         if (mState == STATE_PRINT_CANCELED || mState == STATE_PRINT_CONFIRMED
385                 || mState == STATE_PRINT_COMPLETED) {
386             return true;
387         }
388
389         if (keyCode == KeyEvent.KEYCODE_BACK
390                 && event.isTracking() && !event.isCanceled()) {
391             if (mPrintPreviewController != null && mPrintPreviewController.isOptionsOpened()
392                     && !hasErrors()) {
393                 mPrintPreviewController.closeOptions();
394             } else {
395                 cancelPrint();
396             }
397             return true;
398         }
399         return super.onKeyUp(keyCode, event);
400     }
401
402     @Override
403     public void onRequestContentUpdate() {
404         if (canUpdateDocument()) {
405             updateDocument(false);
406         }
407     }
408
409     @Override
410     public void onMalformedPdfFile() {
411         onPrintDocumentError("Cannot print a malformed PDF file");
412     }
413
414     @Override
415     public void onSecurePdfFile() {
416         onPrintDocumentError("Cannot print a password protected PDF file");
417     }
418
419     private void onPrintDocumentError(String message) {
420         mProgressMessageController.cancel();
421         ensureErrorUiShown(null, PrintErrorFragment.ACTION_RETRY);
422
423         setState(STATE_UPDATE_FAILED);
424
425         updateOptionsUi();
426
427         mPrintedDocument.kill(message);
428     }
429
430     @Override
431     public void onActionPerformed() {
432         if (mState == STATE_UPDATE_FAILED
433                 && canUpdateDocument() && updateDocument(true)) {
434             ensurePreviewUiShown();
435             setState(STATE_CONFIGURING);
436             updateOptionsUi();
437         }
438     }
439
440     public void onUpdateCanceled() {
441         if (DEBUG) {
442             Log.i(LOG_TAG, "onUpdateCanceled()");
443         }
444
445         mProgressMessageController.cancel();
446         ensurePreviewUiShown();
447
448         switch (mState) {
449             case STATE_PRINT_CONFIRMED: {
450                 requestCreatePdfFileOrFinish();
451             } break;
452
453             case STATE_PRINT_CANCELED: {
454                 doFinish();
455             } break;
456         }
457     }
458
459     @Override
460     public void onUpdateCompleted(RemotePrintDocumentInfo document) {
461         if (DEBUG) {
462             Log.i(LOG_TAG, "onUpdateCompleted()");
463         }
464
465         mProgressMessageController.cancel();
466         ensurePreviewUiShown();
467
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;
474         if (info != null) {
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)
480                     .build();
481             mPrintJob.setDocumentInfo(adjustedInfo);
482             mPrintJob.setPages(document.printedPages);
483         }
484
485         switch (mState) {
486             case STATE_PRINT_CONFIRMED: {
487                 requestCreatePdfFileOrFinish();
488             } break;
489
490             case STATE_PRINT_CANCELED: {
491                 updateOptionsUi();
492             } break;
493
494             default: {
495                 updatePrintPreviewController(document.changed);
496
497                 setState(STATE_CONFIGURING);
498                 updateOptionsUi();
499             } break;
500         }
501     }
502
503     @Override
504     public void onUpdateFailed(CharSequence error) {
505         if (DEBUG) {
506             Log.i(LOG_TAG, "onUpdateFailed()");
507         }
508
509         mProgressMessageController.cancel();
510         ensureErrorUiShown(error, PrintErrorFragment.ACTION_RETRY);
511
512         setState(STATE_UPDATE_FAILED);
513
514         updateOptionsUi();
515     }
516
517     @Override
518     public void onOptionsOpened() {
519         updateSelectedPagesFromPreview();
520     }
521
522     @Override
523     public void onOptionsClosed() {
524         PageRange[] selectedPages = computeSelectedPages();
525         if (!Arrays.equals(mSelectedPages, selectedPages)) {
526             mSelectedPages = selectedPages;
527
528             // Update preview.
529             updatePrintPreviewController(false);
530         }
531
532         // Make sure the IME is not on the way of preview as
533         // the user may have used it to type copies or range.
534         InputMethodManager imm = (InputMethodManager) getSystemService(
535                 Context.INPUT_METHOD_SERVICE);
536         imm.hideSoftInputFromWindow(mDestinationSpinner.getWindowToken(), 0);
537     }
538
539     private void updatePrintPreviewController(boolean contentUpdated) {
540         // If we have not heard from the application, do nothing.
541         RemotePrintDocumentInfo documentInfo = mPrintedDocument.getDocumentInfo();
542         if (!documentInfo.laidout) {
543             return;
544         }
545
546         // Update the preview controller.
547         mPrintPreviewController.onContentUpdated(contentUpdated,
548                 getAdjustedPageCount(documentInfo.info),
549                 mPrintedDocument.getDocumentInfo().writtenPages,
550                 mSelectedPages, mPrintJob.getAttributes().getMediaSize(),
551                 mPrintJob.getAttributes().getMinMargins());
552     }
553
554
555     @Override
556     public boolean canOpenOptions() {
557         return true;
558     }
559
560     @Override
561     public boolean canCloseOptions() {
562         return !hasErrors();
563     }
564
565     @Override
566     public void onConfigurationChanged(Configuration newConfig) {
567         super.onConfigurationChanged(newConfig);
568         if (mPrintPreviewController != null) {
569             mPrintPreviewController.onOrientationChanged();
570         }
571     }
572
573     @Override
574     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
575         switch (requestCode) {
576             case ACTIVITY_REQUEST_CREATE_FILE: {
577                 onStartCreateDocumentActivityResult(resultCode, data);
578             } break;
579
580             case ACTIVITY_REQUEST_SELECT_PRINTER: {
581                 onSelectPrinterActivityResult(resultCode, data);
582             } break;
583
584             case ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS: {
585                 onAdvancedPrintOptionsActivityResult(resultCode, data);
586             } break;
587         }
588     }
589
590     private void startCreateDocumentActivity() {
591         if (!isResumed()) {
592             return;
593         }
594         PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
595         if (info == null) {
596             return;
597         }
598         Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
599         intent.setType("application/pdf");
600         intent.putExtra(Intent.EXTRA_TITLE, info.getName());
601         intent.putExtra(DocumentsContract.EXTRA_PACKAGE_NAME, mCallingPackageName);
602         startActivityForResult(intent, ACTIVITY_REQUEST_CREATE_FILE);
603     }
604
605     private void onStartCreateDocumentActivityResult(int resultCode, Intent data) {
606         if (resultCode == RESULT_OK && data != null) {
607             setState(STATE_PRINT_COMPLETED);
608             updateOptionsUi();
609             final Uri uri = data.getData();
610             // Calling finish here does not invoke lifecycle callbacks but we
611             // update the print job in onPause if finishing, hence post a message.
612             mDestinationSpinner.post(new Runnable() {
613                 @Override
614                 public void run() {
615                     transformDocumentAndFinish(uri);
616                 }
617             });
618         } else if (resultCode == RESULT_CANCELED) {
619             mState = STATE_CONFIGURING;
620             updateOptionsUi();
621         } else {
622             setState(STATE_CREATE_FILE_FAILED);
623             updateOptionsUi();
624             // Calling finish here does not invoke lifecycle callbacks but we
625             // update the print job in onPause if finishing, hence post a message.
626             mDestinationSpinner.post(new Runnable() {
627                 @Override
628                 public void run() {
629                     doFinish();
630                 }
631             });
632         }
633     }
634
635     private void startSelectPrinterActivity() {
636         Intent intent = new Intent(this, SelectPrinterActivity.class);
637         startActivityForResult(intent, ACTIVITY_REQUEST_SELECT_PRINTER);
638     }
639
640     private void onSelectPrinterActivityResult(int resultCode, Intent data) {
641         if (resultCode == RESULT_OK && data != null) {
642             PrinterId printerId = data.getParcelableExtra(INTENT_EXTRA_PRINTER_ID);
643             if (printerId != null) {
644                 mDestinationSpinnerAdapter.ensurePrinterInVisibleAdapterPosition(printerId);
645                 final int index = mDestinationSpinnerAdapter.getPrinterIndex(printerId);
646                 if (index != AdapterView.INVALID_POSITION) {
647                     mDestinationSpinner.setSelection(index);
648                     return;
649                 }
650             }
651         }
652
653         PrinterId printerId = mCurrentPrinter.getId();
654         final int index = mDestinationSpinnerAdapter.getPrinterIndex(printerId);
655         mDestinationSpinner.setSelection(index);
656     }
657
658     private void startAdvancedPrintOptionsActivity(PrinterInfo printer) {
659         ComponentName serviceName = printer.getId().getServiceName();
660
661         String activityName = PrintOptionUtils.getAdvancedOptionsActivityName(this, serviceName);
662         if (TextUtils.isEmpty(activityName)) {
663             return;
664         }
665
666         Intent intent = new Intent(Intent.ACTION_MAIN);
667         intent.setComponent(new ComponentName(serviceName.getPackageName(), activityName));
668
669         List<ResolveInfo> resolvedActivities = getPackageManager()
670                 .queryIntentActivities(intent, 0);
671         if (resolvedActivities.isEmpty()) {
672             return;
673         }
674
675         // The activity is a component name, therefore it is one or none.
676         if (resolvedActivities.get(0).activityInfo.exported) {
677             intent.putExtra(PrintService.EXTRA_PRINT_JOB_INFO, mPrintJob);
678             intent.putExtra(PrintService.EXTRA_PRINTER_INFO, printer);
679
680             // This is external activity and may not be there.
681             try {
682                 startActivityForResult(intent, ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS);
683             } catch (ActivityNotFoundException anfe) {
684                 Log.e(LOG_TAG, "Error starting activity for intent: " + intent, anfe);
685             }
686         }
687     }
688
689     private void onAdvancedPrintOptionsActivityResult(int resultCode, Intent data) {
690         if (resultCode != RESULT_OK || data == null) {
691             return;
692         }
693
694         PrintJobInfo printJobInfo = data.getParcelableExtra(PrintService.EXTRA_PRINT_JOB_INFO);
695
696         if (printJobInfo == null) {
697             return;
698         }
699
700         // Take the advanced options without interpretation.
701         mPrintJob.setAdvancedOptions(printJobInfo.getAdvancedOptions());
702
703         // Take copies without interpretation as the advanced print dialog
704         // cannot create a print job info with invalid copies.
705         mCopiesEditText.setText(String.valueOf(printJobInfo.getCopies()));
706         mPrintJob.setCopies(printJobInfo.getCopies());
707
708         PrintAttributes currAttributes = mPrintJob.getAttributes();
709         PrintAttributes newAttributes = printJobInfo.getAttributes();
710
711         if (newAttributes != null) {
712             // Take the media size only if the current printer supports is.
713             MediaSize oldMediaSize = currAttributes.getMediaSize();
714             MediaSize newMediaSize = newAttributes.getMediaSize();
715             if (!oldMediaSize.equals(newMediaSize)) {
716                 final int mediaSizeCount = mMediaSizeSpinnerAdapter.getCount();
717                 MediaSize newMediaSizePortrait = newAttributes.getMediaSize().asPortrait();
718                 for (int i = 0; i < mediaSizeCount; i++) {
719                     MediaSize supportedSizePortrait = mMediaSizeSpinnerAdapter.getItem(i)
720                             .value.asPortrait();
721                     if (supportedSizePortrait.equals(newMediaSizePortrait)) {
722                         currAttributes.setMediaSize(newMediaSize);
723                         mMediaSizeSpinner.setSelection(i);
724                         if (currAttributes.getMediaSize().isPortrait()) {
725                             if (mOrientationSpinner.getSelectedItemPosition() != 0) {
726                                 mOrientationSpinner.setSelection(0);
727                             }
728                         } else {
729                             if (mOrientationSpinner.getSelectedItemPosition() != 1) {
730                                 mOrientationSpinner.setSelection(1);
731                             }
732                         }
733                         break;
734                     }
735                 }
736             }
737
738             // Take the resolution only if the current printer supports is.
739             Resolution oldResolution = currAttributes.getResolution();
740             Resolution newResolution = newAttributes.getResolution();
741             if (!oldResolution.equals(newResolution)) {
742                 PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
743                 if (capabilities != null) {
744                     List<Resolution> resolutions = capabilities.getResolutions();
745                     final int resolutionCount = resolutions.size();
746                     for (int i = 0; i < resolutionCount; i++) {
747                         Resolution resolution = resolutions.get(i);
748                         if (resolution.equals(newResolution)) {
749                             currAttributes.setResolution(resolution);
750                             break;
751                         }
752                     }
753                 }
754             }
755
756             // Take the color mode only if the current printer supports it.
757             final int currColorMode = currAttributes.getColorMode();
758             final int newColorMode = newAttributes.getColorMode();
759             if (currColorMode != newColorMode) {
760                 final int colorModeCount = mColorModeSpinner.getCount();
761                 for (int i = 0; i < colorModeCount; i++) {
762                     final int supportedColorMode = mColorModeSpinnerAdapter.getItem(i).value;
763                     if (supportedColorMode == newColorMode) {
764                         currAttributes.setColorMode(newColorMode);
765                         mColorModeSpinner.setSelection(i);
766                         break;
767                     }
768                 }
769             }
770         }
771
772         // Handle selected page changes making sure they are in the doc.
773         PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
774         final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
775         PageRange[] pageRanges = printJobInfo.getPages();
776         if (pageRanges != null && pageCount > 0) {
777             pageRanges = PageRangeUtils.normalize(pageRanges);
778
779             List<PageRange> validatedList = new ArrayList<>();
780             final int rangeCount = pageRanges.length;
781             for (int i = 0; i < rangeCount; i++) {
782                 PageRange pageRange = pageRanges[i];
783                 if (pageRange.getEnd() >= pageCount) {
784                     final int rangeStart = pageRange.getStart();
785                     final int rangeEnd = pageCount - 1;
786                     if (rangeStart <= rangeEnd) {
787                         pageRange = new PageRange(rangeStart, rangeEnd);
788                         validatedList.add(pageRange);
789                     }
790                     break;
791                 }
792                 validatedList.add(pageRange);
793             }
794
795             if (!validatedList.isEmpty()) {
796                 PageRange[] validatedArray = new PageRange[validatedList.size()];
797                 validatedList.toArray(validatedArray);
798                 updateSelectedPages(validatedArray, pageCount);
799             }
800         }
801
802         // Update the content if needed.
803         if (canUpdateDocument()) {
804             updateDocument(false);
805         }
806     }
807
808     private void setState(int state) {
809         if (isFinalState(mState)) {
810             if (isFinalState(state)) {
811                 mState = state;
812             }
813         } else {
814             mState = state;
815         }
816     }
817
818     private static boolean isFinalState(int state) {
819         return state == STATE_PRINT_CONFIRMED
820                 || state == STATE_PRINT_CANCELED
821                 || state == STATE_PRINT_COMPLETED;
822     }
823
824     private void updateSelectedPagesFromPreview() {
825         PageRange[] selectedPages = mPrintPreviewController.getSelectedPages();
826         if (!Arrays.equals(mSelectedPages, selectedPages)) {
827             updateSelectedPages(selectedPages,
828                     getAdjustedPageCount(mPrintedDocument.getDocumentInfo().info));
829         }
830     }
831
832     private void updateSelectedPages(PageRange[] selectedPages, int pageInDocumentCount) {
833         if (selectedPages == null || selectedPages.length <= 0) {
834             return;
835         }
836
837         selectedPages = PageRangeUtils.normalize(selectedPages);
838
839         // Handle the case where all pages are specified explicitly
840         // instead of the *all pages* constant.
841         if (PageRangeUtils.isAllPages(selectedPages, pageInDocumentCount)) {
842             selectedPages = new PageRange[] {PageRange.ALL_PAGES};
843         }
844
845         if (Arrays.equals(mSelectedPages, selectedPages)) {
846             return;
847         }
848
849         mSelectedPages = selectedPages;
850         mPrintJob.setPages(selectedPages);
851
852         if (Arrays.equals(selectedPages, ALL_PAGES_ARRAY)) {
853             if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
854                 mRangeOptionsSpinner.setSelection(0);
855                 mPageRangeEditText.setText("");
856             }
857         } else if (selectedPages[0].getStart() >= 0
858                 && selectedPages[selectedPages.length - 1].getEnd() < pageInDocumentCount) {
859             if (mRangeOptionsSpinner.getSelectedItemPosition() != 1) {
860                 mRangeOptionsSpinner.setSelection(1);
861             }
862
863             StringBuilder builder = new StringBuilder();
864             final int pageRangeCount = selectedPages.length;
865             for (int i = 0; i < pageRangeCount; i++) {
866                 if (builder.length() > 0) {
867                     builder.append(',');
868                 }
869
870                 final int shownStartPage;
871                 final int shownEndPage;
872                 PageRange pageRange = selectedPages[i];
873                 if (pageRange.equals(PageRange.ALL_PAGES)) {
874                     shownStartPage = 1;
875                     shownEndPage = pageInDocumentCount;
876                 } else {
877                     shownStartPage = pageRange.getStart() + 1;
878                     shownEndPage = pageRange.getEnd() + 1;
879                 }
880
881                 builder.append(shownStartPage);
882
883                 if (shownStartPage != shownEndPage) {
884                     builder.append('-');
885                     builder.append(shownEndPage);
886                 }
887             }
888
889             mPageRangeEditText.setText(builder.toString());
890         }
891     }
892
893     private void ensureProgressUiShown() {
894         if (isFinishing()) {
895             return;
896         }
897         if (mUiState != UI_STATE_PROGRESS) {
898             mUiState = UI_STATE_PROGRESS;
899             mPrintPreviewController.setUiShown(false);
900             Fragment fragment = PrintProgressFragment.newInstance();
901             showFragment(fragment);
902         }
903     }
904
905     private void ensurePreviewUiShown() {
906         if (isFinishing()) {
907             return;
908         }
909         if (mUiState != UI_STATE_PREVIEW) {
910             mUiState = UI_STATE_PREVIEW;
911             mPrintPreviewController.setUiShown(true);
912             showFragment(null);
913         }
914     }
915
916     private void ensureErrorUiShown(CharSequence message, int action) {
917         if (isFinishing()) {
918             return;
919         }
920         if (mUiState != UI_STATE_ERROR) {
921             mUiState = UI_STATE_ERROR;
922             mPrintPreviewController.setUiShown(false);
923             Fragment fragment = PrintErrorFragment.newInstance(message, action);
924             showFragment(fragment);
925         }
926     }
927
928     private void showFragment(Fragment newFragment) {
929         FragmentTransaction transaction = getFragmentManager().beginTransaction();
930         Fragment oldFragment = getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
931         if (oldFragment != null) {
932             transaction.remove(oldFragment);
933         }
934         if (newFragment != null) {
935             transaction.add(R.id.embedded_content_container, newFragment, FRAGMENT_TAG);
936         }
937         transaction.commit();
938         getFragmentManager().executePendingTransactions();
939     }
940
941     private void requestCreatePdfFileOrFinish() {
942         if (mCurrentPrinter == mDestinationSpinnerAdapter.getPdfPrinter()) {
943             startCreateDocumentActivity();
944         } else {
945             transformDocumentAndFinish(null);
946         }
947     }
948
949     private void updatePrintAttributesFromCapabilities(PrinterCapabilitiesInfo capabilities) {
950         PrintAttributes defaults = capabilities.getDefaults();
951
952         // Sort the media sizes based on the current locale.
953         List<MediaSize> sortedMediaSizes = new ArrayList<>(capabilities.getMediaSizes());
954         Collections.sort(sortedMediaSizes, mMediaSizeComparator);
955
956         PrintAttributes attributes = mPrintJob.getAttributes();
957
958         // Media size.
959         MediaSize currMediaSize = attributes.getMediaSize();
960         if (currMediaSize == null) {
961             attributes.setMediaSize(defaults.getMediaSize());
962         } else {
963             boolean foundCurrentMediaSize = false;
964             // Try to find the current media size in the capabilities as
965             // it may be in a different orientation.
966             MediaSize currMediaSizePortrait = currMediaSize.asPortrait();
967             final int mediaSizeCount = sortedMediaSizes.size();
968             for (int i = 0; i < mediaSizeCount; i++) {
969                 MediaSize mediaSize = sortedMediaSizes.get(i);
970                 if (currMediaSizePortrait.equals(mediaSize.asPortrait())) {
971                     attributes.setMediaSize(currMediaSize);
972                     foundCurrentMediaSize = true;
973                     break;
974                 }
975             }
976             // If we did not find the current media size fall back to default.
977             if (!foundCurrentMediaSize) {
978                 attributes.setMediaSize(defaults.getMediaSize());
979             }
980         }
981
982         // Color mode.
983         final int colorMode = attributes.getColorMode();
984         if ((capabilities.getColorModes() & colorMode) == 0) {
985             attributes.setColorMode(defaults.getColorMode());
986         }
987
988         // Resolution
989         Resolution resolution = attributes.getResolution();
990         if (resolution == null || !capabilities.getResolutions().contains(resolution)) {
991             attributes.setResolution(defaults.getResolution());
992         }
993
994         // Margins.
995         attributes.setMinMargins(defaults.getMinMargins());
996     }
997
998     private boolean updateDocument(boolean clearLastError) {
999         if (!clearLastError && mPrintedDocument.hasUpdateError()) {
1000             return false;
1001         }
1002
1003         if (clearLastError && mPrintedDocument.hasUpdateError()) {
1004             mPrintedDocument.clearUpdateError();
1005         }
1006
1007         final boolean preview = mState != STATE_PRINT_CONFIRMED;
1008         final PageRange[] pages;
1009         if (preview) {
1010             pages = mPrintPreviewController.getRequestedPages();
1011         } else {
1012             pages = mPrintPreviewController.getSelectedPages();
1013         }
1014
1015         final boolean willUpdate = mPrintedDocument.update(mPrintJob.getAttributes(),
1016                 pages, preview);
1017
1018         if (willUpdate && !mPrintedDocument.hasLaidOutPages()) {
1019             // When the update is done we update the print preview.
1020             mProgressMessageController.post();
1021             return true;
1022         } else if (!willUpdate) {
1023             // Update preview.
1024             updatePrintPreviewController(false);
1025         }
1026
1027         return false;
1028     }
1029
1030     private void addCurrentPrinterToHistory() {
1031         if (mCurrentPrinter != null) {
1032             PrinterId fakePdfPrinterId = mDestinationSpinnerAdapter.getPdfPrinter().getId();
1033             if (!mCurrentPrinter.getId().equals(fakePdfPrinterId)) {
1034                 mPrinterRegistry.addHistoricalPrinter(mCurrentPrinter);
1035             }
1036         }
1037     }
1038
1039     private void cancelPrint() {
1040         setState(STATE_PRINT_CANCELED);
1041         updateOptionsUi();
1042         if (mPrintedDocument.isUpdating()) {
1043             mPrintedDocument.cancel();
1044         }
1045         doFinish();
1046     }
1047
1048     private void confirmPrint() {
1049         setState(STATE_PRINT_CONFIRMED);
1050
1051         updateOptionsUi();
1052         addCurrentPrinterToHistory();
1053
1054         PageRange[] selectedPages = computeSelectedPages();
1055         if (!Arrays.equals(mSelectedPages, selectedPages)) {
1056             mSelectedPages = selectedPages;
1057             // Update preview.
1058             updatePrintPreviewController(false);
1059         }
1060
1061         updateSelectedPagesFromPreview();
1062         mPrintPreviewController.closeOptions();
1063
1064         if (canUpdateDocument()) {
1065             updateDocument(false);
1066         }
1067
1068         if (!mPrintedDocument.isUpdating()) {
1069             requestCreatePdfFileOrFinish();
1070         }
1071     }
1072
1073     private void bindUi() {
1074         // Summary
1075         mSummaryContainer = findViewById(R.id.summary_content);
1076         mSummaryCopies = (TextView) findViewById(R.id.copies_count_summary);
1077         mSummaryPaperSize = (TextView) findViewById(R.id.paper_size_summary);
1078
1079         // Options container
1080         mOptionsContent = (PrintContentView) findViewById(R.id.options_content);
1081         mOptionsContent.setOptionsStateChangeListener(this);
1082         mOptionsContent.setOpenOptionsController(this);
1083
1084         OnItemSelectedListener itemSelectedListener = new MyOnItemSelectedListener();
1085         OnClickListener clickListener = new MyClickListener();
1086
1087         // Copies
1088         mCopiesEditText = (EditText) findViewById(R.id.copies_edittext);
1089         mCopiesEditText.setOnFocusChangeListener(mSelectAllOnFocusListener);
1090         mCopiesEditText.setText(MIN_COPIES_STRING);
1091         mCopiesEditText.setSelection(mCopiesEditText.getText().length());
1092         mCopiesEditText.addTextChangedListener(new EditTextWatcher());
1093
1094         // Destination.
1095         mDestinationSpinnerAdapter.registerDataSetObserver(new PrintersObserver());
1096         mDestinationSpinner = (Spinner) findViewById(R.id.destination_spinner);
1097         mDestinationSpinner.setAdapter(mDestinationSpinnerAdapter);
1098         mDestinationSpinner.setOnItemSelectedListener(itemSelectedListener);
1099
1100         // Media size.
1101         mMediaSizeSpinnerAdapter = new ArrayAdapter<>(
1102                 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1103         mMediaSizeSpinner = (Spinner) findViewById(R.id.paper_size_spinner);
1104         mMediaSizeSpinner.setAdapter(mMediaSizeSpinnerAdapter);
1105         mMediaSizeSpinner.setOnItemSelectedListener(itemSelectedListener);
1106
1107         // Color mode.
1108         mColorModeSpinnerAdapter = new ArrayAdapter<>(
1109                 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1110         mColorModeSpinner = (Spinner) findViewById(R.id.color_spinner);
1111         mColorModeSpinner.setAdapter(mColorModeSpinnerAdapter);
1112         mColorModeSpinner.setOnItemSelectedListener(itemSelectedListener);
1113
1114         // Orientation
1115         mOrientationSpinnerAdapter = new ArrayAdapter<>(
1116                 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1117         String[] orientationLabels = getResources().getStringArray(
1118                 R.array.orientation_labels);
1119         mOrientationSpinnerAdapter.add(new SpinnerItem<>(
1120                 ORIENTATION_PORTRAIT, orientationLabels[0]));
1121         mOrientationSpinnerAdapter.add(new SpinnerItem<>(
1122                 ORIENTATION_LANDSCAPE, orientationLabels[1]));
1123         mOrientationSpinner = (Spinner) findViewById(R.id.orientation_spinner);
1124         mOrientationSpinner.setAdapter(mOrientationSpinnerAdapter);
1125         mOrientationSpinner.setOnItemSelectedListener(itemSelectedListener);
1126
1127         // Range options
1128         ArrayAdapter<SpinnerItem<Integer>> rangeOptionsSpinnerAdapter = new ArrayAdapter<>(
1129                 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1130         mRangeOptionsSpinner = (Spinner) findViewById(R.id.range_options_spinner);
1131         mRangeOptionsSpinner.setAdapter(rangeOptionsSpinnerAdapter);
1132         mRangeOptionsSpinner.setOnItemSelectedListener(itemSelectedListener);
1133         updatePageRangeOptions(PrintDocumentInfo.PAGE_COUNT_UNKNOWN);
1134
1135         // Page range
1136         mPageRangeTitle = (TextView) findViewById(R.id.page_range_title);
1137         mPageRangeEditText = (EditText) findViewById(R.id.page_range_edittext);
1138         mPageRangeEditText.setOnFocusChangeListener(mSelectAllOnFocusListener);
1139         mPageRangeEditText.addTextChangedListener(new RangeTextWatcher());
1140
1141         // Advanced options button.
1142         mMoreOptionsButton = (Button) findViewById(R.id.more_options_button);
1143         mMoreOptionsButton.setOnClickListener(clickListener);
1144
1145         // Print button
1146         mPrintButton = (ImageView) findViewById(R.id.print_button);
1147         mPrintButton.setOnClickListener(clickListener);
1148     }
1149
1150     private final class MyClickListener implements OnClickListener {
1151         @Override
1152         public void onClick(View view) {
1153             if (view == mPrintButton) {
1154                 if (mCurrentPrinter != null) {
1155                     confirmPrint();
1156                 } else {
1157                     cancelPrint();
1158                 }
1159             } else if (view == mMoreOptionsButton) {
1160                 if (mCurrentPrinter != null) {
1161                     startAdvancedPrintOptionsActivity(mCurrentPrinter);
1162                 }
1163             }
1164         }
1165     }
1166
1167     private static boolean canPrint(PrinterInfo printer) {
1168         return printer.getCapabilities() != null
1169                 && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
1170     }
1171
1172     void updateOptionsUi() {
1173         // Always update the summary.
1174         updateSummary();
1175
1176         if (mState == STATE_PRINT_CONFIRMED
1177                 || mState == STATE_PRINT_COMPLETED
1178                 || mState == STATE_PRINT_CANCELED
1179                 || mState == STATE_UPDATE_FAILED
1180                 || mState == STATE_CREATE_FILE_FAILED
1181                 || mState == STATE_PRINTER_UNAVAILABLE
1182                 || mState == STATE_UPDATE_SLOW) {
1183             if (mState != STATE_PRINTER_UNAVAILABLE) {
1184                 mDestinationSpinner.setEnabled(false);
1185             }
1186             mCopiesEditText.setEnabled(false);
1187             mCopiesEditText.setFocusable(false);
1188             mMediaSizeSpinner.setEnabled(false);
1189             mColorModeSpinner.setEnabled(false);
1190             mOrientationSpinner.setEnabled(false);
1191             mRangeOptionsSpinner.setEnabled(false);
1192             mPageRangeEditText.setEnabled(false);
1193             mPrintButton.setVisibility(View.GONE);
1194             mMoreOptionsButton.setEnabled(false);
1195             return;
1196         }
1197
1198         // If no current printer, or it has no capabilities, or it is not
1199         // available, we disable all print options except the destination.
1200         if (mCurrentPrinter == null || !canPrint(mCurrentPrinter)) {
1201             mCopiesEditText.setEnabled(false);
1202             mCopiesEditText.setFocusable(false);
1203             mMediaSizeSpinner.setEnabled(false);
1204             mColorModeSpinner.setEnabled(false);
1205             mOrientationSpinner.setEnabled(false);
1206             mRangeOptionsSpinner.setEnabled(false);
1207             mPageRangeEditText.setEnabled(false);
1208             mPrintButton.setVisibility(View.GONE);
1209             mMoreOptionsButton.setEnabled(false);
1210             return;
1211         }
1212
1213         PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
1214         PrintAttributes defaultAttributes = capabilities.getDefaults();
1215
1216         // Destination.
1217         mDestinationSpinner.setEnabled(true);
1218
1219         // Media size.
1220         mMediaSizeSpinner.setEnabled(true);
1221
1222         List<MediaSize> mediaSizes = new ArrayList<>(capabilities.getMediaSizes());
1223         // Sort the media sizes based on the current locale.
1224         Collections.sort(mediaSizes, mMediaSizeComparator);
1225
1226         PrintAttributes attributes = mPrintJob.getAttributes();
1227
1228         // If the media sizes changed, we update the adapter and the spinner.
1229         boolean mediaSizesChanged = false;
1230         final int mediaSizeCount = mediaSizes.size();
1231         if (mediaSizeCount != mMediaSizeSpinnerAdapter.getCount()) {
1232             mediaSizesChanged = true;
1233         } else {
1234             for (int i = 0; i < mediaSizeCount; i++) {
1235                 if (!mediaSizes.get(i).equals(mMediaSizeSpinnerAdapter.getItem(i).value)) {
1236                     mediaSizesChanged = true;
1237                     break;
1238                 }
1239             }
1240         }
1241         if (mediaSizesChanged) {
1242             // Remember the old media size to try selecting it again.
1243             int oldMediaSizeNewIndex = AdapterView.INVALID_POSITION;
1244             MediaSize oldMediaSize = attributes.getMediaSize();
1245
1246             // Rebuild the adapter data.
1247             mMediaSizeSpinnerAdapter.clear();
1248             for (int i = 0; i < mediaSizeCount; i++) {
1249                 MediaSize mediaSize = mediaSizes.get(i);
1250                 if (oldMediaSize != null
1251                         && mediaSize.asPortrait().equals(oldMediaSize.asPortrait())) {
1252                     // Update the index of the old selection.
1253                     oldMediaSizeNewIndex = i;
1254                 }
1255                 mMediaSizeSpinnerAdapter.add(new SpinnerItem<>(
1256                         mediaSize, mediaSize.getLabel(getPackageManager())));
1257             }
1258
1259             if (oldMediaSizeNewIndex != AdapterView.INVALID_POSITION) {
1260                 // Select the old media size - nothing really changed.
1261                 if (mMediaSizeSpinner.getSelectedItemPosition() != oldMediaSizeNewIndex) {
1262                     mMediaSizeSpinner.setSelection(oldMediaSizeNewIndex);
1263                 }
1264             } else {
1265                 // Select the first or the default.
1266                 final int mediaSizeIndex = Math.max(mediaSizes.indexOf(
1267                         defaultAttributes.getMediaSize()), 0);
1268                 if (mMediaSizeSpinner.getSelectedItemPosition() != mediaSizeIndex) {
1269                     mMediaSizeSpinner.setSelection(mediaSizeIndex);
1270                 }
1271                 // Respect the orientation of the old selection.
1272                 if (oldMediaSize != null) {
1273                     if (oldMediaSize.isPortrait()) {
1274                         attributes.setMediaSize(mMediaSizeSpinnerAdapter
1275                                 .getItem(mediaSizeIndex).value.asPortrait());
1276                     } else {
1277                         attributes.setMediaSize(mMediaSizeSpinnerAdapter
1278                                 .getItem(mediaSizeIndex).value.asLandscape());
1279                     }
1280                 }
1281             }
1282         }
1283
1284         // Color mode.
1285         mColorModeSpinner.setEnabled(true);
1286         final int colorModes = capabilities.getColorModes();
1287
1288         // If the color modes changed, we update the adapter and the spinner.
1289         boolean colorModesChanged = false;
1290         if (Integer.bitCount(colorModes) != mColorModeSpinnerAdapter.getCount()) {
1291             colorModesChanged = true;
1292         } else {
1293             int remainingColorModes = colorModes;
1294             int adapterIndex = 0;
1295             while (remainingColorModes != 0) {
1296                 final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
1297                 final int colorMode = 1 << colorBitOffset;
1298                 remainingColorModes &= ~colorMode;
1299                 if (colorMode != mColorModeSpinnerAdapter.getItem(adapterIndex).value) {
1300                     colorModesChanged = true;
1301                     break;
1302                 }
1303                 adapterIndex++;
1304             }
1305         }
1306         if (colorModesChanged) {
1307             // Remember the old color mode to try selecting it again.
1308             int oldColorModeNewIndex = AdapterView.INVALID_POSITION;
1309             final int oldColorMode = attributes.getColorMode();
1310
1311             // Rebuild the adapter data.
1312             mColorModeSpinnerAdapter.clear();
1313             String[] colorModeLabels = getResources().getStringArray(R.array.color_mode_labels);
1314             int remainingColorModes = colorModes;
1315             while (remainingColorModes != 0) {
1316                 final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
1317                 final int colorMode = 1 << colorBitOffset;
1318                 if (colorMode == oldColorMode) {
1319                     // Update the index of the old selection.
1320                     oldColorModeNewIndex = colorBitOffset;
1321                 }
1322                 remainingColorModes &= ~colorMode;
1323                 mColorModeSpinnerAdapter.add(new SpinnerItem<>(colorMode,
1324                         colorModeLabels[colorBitOffset]));
1325             }
1326             if (oldColorModeNewIndex != AdapterView.INVALID_POSITION) {
1327                 // Select the old color mode - nothing really changed.
1328                 if (mColorModeSpinner.getSelectedItemPosition() != oldColorModeNewIndex) {
1329                     mColorModeSpinner.setSelection(oldColorModeNewIndex);
1330                 }
1331             } else {
1332                 // Select the default.
1333                 final int selectedColorMode = colorModes & defaultAttributes.getColorMode();
1334                 final int itemCount = mColorModeSpinnerAdapter.getCount();
1335                 for (int i = 0; i < itemCount; i++) {
1336                     SpinnerItem<Integer> item = mColorModeSpinnerAdapter.getItem(i);
1337                     if (selectedColorMode == item.value) {
1338                         if (mColorModeSpinner.getSelectedItemPosition() != i) {
1339                             mColorModeSpinner.setSelection(i);
1340                         }
1341                         attributes.setColorMode(selectedColorMode);
1342                     }
1343                 }
1344             }
1345         }
1346
1347         // Orientation
1348         mOrientationSpinner.setEnabled(true);
1349         MediaSize mediaSize = attributes.getMediaSize();
1350         if (mediaSize != null) {
1351             if (mediaSize.isPortrait()
1352                     && mOrientationSpinner.getSelectedItemPosition() != 0) {
1353                 mOrientationSpinner.setSelection(0);
1354             } else if (!mediaSize.isPortrait()
1355                     && mOrientationSpinner.getSelectedItemPosition() != 1) {
1356                 mOrientationSpinner.setSelection(1);
1357             }
1358         }
1359
1360         // Range options
1361         PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
1362         final int pageCount = getAdjustedPageCount(info);
1363         if (info != null && pageCount > 0) {
1364             if (pageCount == 1) {
1365                 mRangeOptionsSpinner.setEnabled(false);
1366             } else {
1367                 mRangeOptionsSpinner.setEnabled(true);
1368                 if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
1369                     if (!mPageRangeEditText.isEnabled()) {
1370                         mPageRangeEditText.setEnabled(true);
1371                         mPageRangeEditText.setVisibility(View.VISIBLE);
1372                         mPageRangeTitle.setVisibility(View.VISIBLE);
1373                         mPageRangeEditText.requestFocus();
1374                         InputMethodManager imm = (InputMethodManager)
1375                                 getSystemService(Context.INPUT_METHOD_SERVICE);
1376                         imm.showSoftInput(mPageRangeEditText, 0);
1377                     }
1378                 } else {
1379                     mPageRangeEditText.setEnabled(false);
1380                     mPageRangeEditText.setVisibility(View.INVISIBLE);
1381                     mPageRangeTitle.setVisibility(View.INVISIBLE);
1382                 }
1383             }
1384         } else {
1385             if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
1386                 mRangeOptionsSpinner.setSelection(0);
1387                 mPageRangeEditText.setText("");
1388             }
1389             mRangeOptionsSpinner.setEnabled(false);
1390             mPageRangeEditText.setEnabled(false);
1391             mPageRangeEditText.setVisibility(View.INVISIBLE);
1392             mPageRangeTitle.setVisibility(View.INVISIBLE);
1393         }
1394
1395         final int newPageCount = getAdjustedPageCount(info);
1396         if (newPageCount != mCurrentPageCount) {
1397             mCurrentPageCount = newPageCount;
1398             updatePageRangeOptions(newPageCount);
1399         }
1400
1401         // Advanced print options
1402         ComponentName serviceName = mCurrentPrinter.getId().getServiceName();
1403         if (!TextUtils.isEmpty(PrintOptionUtils.getAdvancedOptionsActivityName(
1404                 this, serviceName))) {
1405             mMoreOptionsButton.setVisibility(View.VISIBLE);
1406             mMoreOptionsButton.setEnabled(true);
1407         } else {
1408             mMoreOptionsButton.setVisibility(View.GONE);
1409             mMoreOptionsButton.setEnabled(false);
1410         }
1411
1412         // Print
1413         if (mDestinationSpinnerAdapter.getPdfPrinter() != mCurrentPrinter) {
1414             mPrintButton.setImageResource(com.android.internal.R.drawable.ic_print);
1415             mPrintButton.setContentDescription(getString(R.string.print_button));
1416         } else {
1417             mPrintButton.setImageResource(R.drawable.ic_menu_savetopdf);
1418             mPrintButton.setContentDescription(getString(R.string.savetopdf_button));
1419         }
1420         if (!mPrintedDocument.getDocumentInfo().laidout
1421                 ||(mRangeOptionsSpinner.getSelectedItemPosition() == 1
1422                 && (TextUtils.isEmpty(mPageRangeEditText.getText()) || hasErrors()))
1423                 || (mRangeOptionsSpinner.getSelectedItemPosition() == 0
1424                 && (mPrintedDocument.getDocumentInfo() == null || hasErrors()))) {
1425             mPrintButton.setVisibility(View.GONE);
1426         } else {
1427             mPrintButton.setVisibility(View.VISIBLE);
1428         }
1429
1430         // Copies
1431         if (mDestinationSpinnerAdapter.getPdfPrinter() != mCurrentPrinter) {
1432             mCopiesEditText.setEnabled(true);
1433             mCopiesEditText.setFocusableInTouchMode(true);
1434         } else {
1435             CharSequence text = mCopiesEditText.getText();
1436             if (TextUtils.isEmpty(text) || !MIN_COPIES_STRING.equals(text.toString())) {
1437                 mCopiesEditText.setText(MIN_COPIES_STRING);
1438             }
1439             mCopiesEditText.setEnabled(false);
1440             mCopiesEditText.setFocusable(false);
1441         }
1442         if (mCopiesEditText.getError() == null
1443                 && TextUtils.isEmpty(mCopiesEditText.getText())) {
1444             mCopiesEditText.setText(MIN_COPIES_STRING);
1445             mCopiesEditText.requestFocus();
1446         }
1447     }
1448
1449     private void updateSummary() {
1450         CharSequence copiesText = null;
1451         CharSequence mediaSizeText = null;
1452
1453         if (!TextUtils.isEmpty(mCopiesEditText.getText())) {
1454             copiesText = mCopiesEditText.getText();
1455             mSummaryCopies.setText(copiesText);
1456         }
1457
1458         final int selectedMediaIndex = mMediaSizeSpinner.getSelectedItemPosition();
1459         if (selectedMediaIndex >= 0) {
1460             SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(selectedMediaIndex);
1461             mediaSizeText = mediaItem.label;
1462             mSummaryPaperSize.setText(mediaSizeText);
1463         }
1464
1465         if (!TextUtils.isEmpty(copiesText) && !TextUtils.isEmpty(mediaSizeText)) {
1466             String summaryText = getString(R.string.summary_template, copiesText, mediaSizeText);
1467             mSummaryContainer.setContentDescription(summaryText);
1468         }
1469     }
1470
1471     private void updatePageRangeOptions(int pageCount) {
1472         ArrayAdapter<SpinnerItem<Integer>> rangeOptionsSpinnerAdapter =
1473                 (ArrayAdapter) mRangeOptionsSpinner.getAdapter();
1474         rangeOptionsSpinnerAdapter.clear();
1475
1476         final int[] rangeOptionsValues = getResources().getIntArray(
1477                 R.array.page_options_values);
1478
1479         String pageCountLabel = (pageCount > 0) ? String.valueOf(pageCount) : "";
1480         String[] rangeOptionsLabels = new String[] {
1481             getString(R.string.template_all_pages, pageCountLabel),
1482             getString(R.string.template_page_range, pageCountLabel)
1483         };
1484
1485         final int rangeOptionsCount = rangeOptionsLabels.length;
1486         for (int i = 0; i < rangeOptionsCount; i++) {
1487             rangeOptionsSpinnerAdapter.add(new SpinnerItem<>(
1488                     rangeOptionsValues[i], rangeOptionsLabels[i]));
1489         }
1490     }
1491
1492     private PageRange[] computeSelectedPages() {
1493         if (hasErrors()) {
1494             return null;
1495         }
1496
1497         if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
1498             List<PageRange> pageRanges = new ArrayList<>();
1499             mStringCommaSplitter.setString(mPageRangeEditText.getText().toString());
1500
1501             while (mStringCommaSplitter.hasNext()) {
1502                 String range = mStringCommaSplitter.next().trim();
1503                 if (TextUtils.isEmpty(range)) {
1504                     continue;
1505                 }
1506                 final int dashIndex = range.indexOf('-');
1507                 final int fromIndex;
1508                 final int toIndex;
1509
1510                 if (dashIndex > 0) {
1511                     fromIndex = Integer.parseInt(range.substring(0, dashIndex).trim()) - 1;
1512                     // It is possible that the dash is at the end since the input
1513                     // verification can has to allow the user to keep entering if
1514                     // this would lead to a valid input. So we handle this.
1515                     if (dashIndex < range.length() - 1) {
1516                         String fromString = range.substring(dashIndex + 1, range.length()).trim();
1517                         toIndex = Integer.parseInt(fromString) - 1;
1518                     } else {
1519                         toIndex = fromIndex;
1520                     }
1521                 } else {
1522                     fromIndex = toIndex = Integer.parseInt(range) - 1;
1523                 }
1524
1525                 PageRange pageRange = new PageRange(Math.min(fromIndex, toIndex),
1526                         Math.max(fromIndex, toIndex));
1527                 pageRanges.add(pageRange);
1528             }
1529
1530             PageRange[] pageRangesArray = new PageRange[pageRanges.size()];
1531             pageRanges.toArray(pageRangesArray);
1532
1533             return PageRangeUtils.normalize(pageRangesArray);
1534         }
1535
1536         return ALL_PAGES_ARRAY;
1537     }
1538
1539     private int getAdjustedPageCount(PrintDocumentInfo info) {
1540         if (info != null) {
1541             final int pageCount = info.getPageCount();
1542             if (pageCount != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) {
1543                 return pageCount;
1544             }
1545         }
1546         // If the app does not tell us how many pages are in the
1547         // doc we ask for all pages and use the document page count.
1548         return mPrintPreviewController.getFilePageCount();
1549     }
1550
1551     private boolean hasErrors() {
1552         return (mCopiesEditText.getError() != null)
1553                 || (mPageRangeEditText.getVisibility() == View.VISIBLE
1554                 && mPageRangeEditText.getError() != null);
1555     }
1556
1557     public void onPrinterAvailable(PrinterInfo printer) {
1558         if (mCurrentPrinter.equals(printer)) {
1559             setState(STATE_CONFIGURING);
1560             if (canUpdateDocument()) {
1561                 updateDocument(false);
1562             }
1563             ensurePreviewUiShown();
1564             updateOptionsUi();
1565         }
1566     }
1567
1568     public void onPrinterUnavailable(PrinterInfo printer) {
1569         if (mCurrentPrinter.getId().equals(printer.getId())) {
1570             setState(STATE_PRINTER_UNAVAILABLE);
1571             if (mPrintedDocument.isUpdating()) {
1572                 mPrintedDocument.cancel();
1573             }
1574             ensureErrorUiShown(getString(R.string.print_error_printer_unavailable),
1575                     PrintErrorFragment.ACTION_NONE);
1576             updateOptionsUi();
1577         }
1578     }
1579
1580     private boolean canUpdateDocument() {
1581         if (mPrintedDocument.isDestroyed()) {
1582             return false;
1583         }
1584
1585         if (hasErrors()) {
1586             return false;
1587         }
1588
1589         PrintAttributes attributes = mPrintJob.getAttributes();
1590
1591         final int colorMode = attributes.getColorMode();
1592         if (colorMode != PrintAttributes.COLOR_MODE_COLOR
1593                 && colorMode != PrintAttributes.COLOR_MODE_MONOCHROME) {
1594             return false;
1595         }
1596         if (attributes.getMediaSize() == null) {
1597             return false;
1598         }
1599         if (attributes.getMinMargins() == null) {
1600             return false;
1601         }
1602         if (attributes.getResolution() == null) {
1603             return false;
1604         }
1605
1606         if (mCurrentPrinter == null) {
1607             return false;
1608         }
1609         PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
1610         if (capabilities == null) {
1611             return false;
1612         }
1613         if (mCurrentPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE) {
1614             return false;
1615         }
1616
1617         return true;
1618     }
1619
1620     private void transformDocumentAndFinish(final Uri writeToUri) {
1621         // If saving to PDF, apply the attibutes as we are acting as a print service.
1622         PrintAttributes attributes = mDestinationSpinnerAdapter.getPdfPrinter() == mCurrentPrinter
1623                 ?  mPrintJob.getAttributes() : null;
1624         new DocumentTransformer(this, mPrintJob, mFileProvider, attributes, new Runnable() {
1625             @Override
1626             public void run() {
1627                 if (writeToUri != null) {
1628                     mPrintedDocument.writeContent(getContentResolver(), writeToUri);
1629                 }
1630                 doFinish();
1631             }
1632         }).transform();
1633     }
1634
1635     private void doFinish() {
1636         if (mState != STATE_INITIALIZING) {
1637             mProgressMessageController.cancel();
1638             mPrinterRegistry.setTrackedPrinter(null);
1639             mSpoolerProvider.destroy();
1640             mPrintedDocument.finish();
1641             mPrintedDocument.destroy();
1642             mPrintPreviewController.destroy(new Runnable() {
1643                 @Override
1644                 public void run() {
1645                     finish();
1646                 }
1647             });
1648         } else {
1649             finish();
1650         }
1651     }
1652
1653     private final class SpinnerItem<T> {
1654         final T value;
1655         final CharSequence label;
1656
1657         public SpinnerItem(T value, CharSequence label) {
1658             this.value = value;
1659             this.label = label;
1660         }
1661
1662         public String toString() {
1663             return label.toString();
1664         }
1665     }
1666
1667     private final class PrinterAvailabilityDetector implements Runnable {
1668         private static final long UNAVAILABLE_TIMEOUT_MILLIS = 10000; // 10sec
1669
1670         private boolean mPosted;
1671
1672         private boolean mPrinterUnavailable;
1673
1674         private PrinterInfo mPrinter;
1675
1676         public void updatePrinter(PrinterInfo printer) {
1677             if (printer.equals(mDestinationSpinnerAdapter.getPdfPrinter())) {
1678                 return;
1679             }
1680
1681             final boolean available = printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE
1682                     && printer.getCapabilities() != null;
1683             final boolean notifyIfAvailable;
1684
1685             if (mPrinter == null || !mPrinter.getId().equals(printer.getId())) {
1686                 notifyIfAvailable = true;
1687                 unpostIfNeeded();
1688                 mPrinterUnavailable = false;
1689                 mPrinter = new PrinterInfo.Builder(printer).build();
1690             } else {
1691                 notifyIfAvailable =
1692                         (mPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE
1693                                 && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE)
1694                                 || (mPrinter.getCapabilities() == null
1695                                 && printer.getCapabilities() != null);
1696                 mPrinter.copyFrom(printer);
1697             }
1698
1699             if (available) {
1700                 unpostIfNeeded();
1701                 mPrinterUnavailable = false;
1702                 if (notifyIfAvailable) {
1703                     onPrinterAvailable(mPrinter);
1704                 }
1705             } else {
1706                 if (!mPrinterUnavailable) {
1707                     postIfNeeded();
1708                 }
1709             }
1710         }
1711
1712         public void cancel() {
1713             unpostIfNeeded();
1714             mPrinterUnavailable = false;
1715         }
1716
1717         private void postIfNeeded() {
1718             if (!mPosted) {
1719                 mPosted = true;
1720                 mDestinationSpinner.postDelayed(this, UNAVAILABLE_TIMEOUT_MILLIS);
1721             }
1722         }
1723
1724         private void unpostIfNeeded() {
1725             if (mPosted) {
1726                 mPosted = false;
1727                 mDestinationSpinner.removeCallbacks(this);
1728             }
1729         }
1730
1731         @Override
1732         public void run() {
1733             mPosted = false;
1734             mPrinterUnavailable = true;
1735             onPrinterUnavailable(mPrinter);
1736         }
1737     }
1738
1739     private static final class PrinterHolder {
1740         PrinterInfo printer;
1741         boolean removed;
1742
1743         public PrinterHolder(PrinterInfo printer) {
1744             this.printer = printer;
1745         }
1746     }
1747
1748     private final class DestinationAdapter extends BaseAdapter
1749             implements PrinterRegistry.OnPrintersChangeListener {
1750         private final List<PrinterHolder> mPrinterHolders = new ArrayList<>();
1751
1752         private final PrinterHolder mFakePdfPrinterHolder;
1753
1754         private boolean mHistoricalPrintersLoaded;
1755
1756         public DestinationAdapter() {
1757             mHistoricalPrintersLoaded = mPrinterRegistry.areHistoricalPrintersLoaded();
1758             if (mHistoricalPrintersLoaded) {
1759                 addPrinters(mPrinterHolders, mPrinterRegistry.getPrinters());
1760             }
1761             mPrinterRegistry.setOnPrintersChangeListener(this);
1762             mFakePdfPrinterHolder = new PrinterHolder(createFakePdfPrinter());
1763         }
1764
1765         public PrinterInfo getPdfPrinter() {
1766             return mFakePdfPrinterHolder.printer;
1767         }
1768
1769         public int getPrinterIndex(PrinterId printerId) {
1770             for (int i = 0; i < getCount(); i++) {
1771                 PrinterHolder printerHolder = (PrinterHolder) getItem(i);
1772                 if (printerHolder != null && !printerHolder.removed
1773                         && printerHolder.printer.getId().equals(printerId)) {
1774                     return i;
1775                 }
1776             }
1777             return AdapterView.INVALID_POSITION;
1778         }
1779
1780         public void ensurePrinterInVisibleAdapterPosition(PrinterId printerId) {
1781             final int printerCount = mPrinterHolders.size();
1782             for (int i = 0; i < printerCount; i++) {
1783                 PrinterHolder printerHolder = mPrinterHolders.get(i);
1784                 if (printerHolder.printer.getId().equals(printerId)) {
1785                     // If already in the list - do nothing.
1786                     if (i < getCount() - 2) {
1787                         return;
1788                     }
1789                     // Else replace the last one (two items are not printers).
1790                     final int lastPrinterIndex = getCount() - 3;
1791                     mPrinterHolders.set(i, mPrinterHolders.get(lastPrinterIndex));
1792                     mPrinterHolders.set(lastPrinterIndex, printerHolder);
1793                     notifyDataSetChanged();
1794                     return;
1795                 }
1796             }
1797         }
1798
1799         @Override
1800         public int getCount() {
1801             if (mHistoricalPrintersLoaded) {
1802                 return Math.min(mPrinterHolders.size() + 2, DEST_ADAPTER_MAX_ITEM_COUNT);
1803             }
1804             return 0;
1805         }
1806
1807         @Override
1808         public boolean isEnabled(int position) {
1809             Object item = getItem(position);
1810             if (item instanceof PrinterHolder) {
1811                 PrinterHolder printerHolder = (PrinterHolder) item;
1812                 return !printerHolder.removed
1813                         && printerHolder.printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
1814             }
1815             return true;
1816         }
1817
1818         @Override
1819         public Object getItem(int position) {
1820             if (mPrinterHolders.isEmpty()) {
1821                 if (position == 0) {
1822                     return mFakePdfPrinterHolder;
1823                 }
1824             } else {
1825                 if (position < 1) {
1826                     return mPrinterHolders.get(position);
1827                 }
1828                 if (position == 1) {
1829                     return mFakePdfPrinterHolder;
1830                 }
1831                 if (position < getCount() - 1) {
1832                     return mPrinterHolders.get(position - 1);
1833                 }
1834             }
1835             return null;
1836         }
1837
1838         @Override
1839         public long getItemId(int position) {
1840             if (mPrinterHolders.isEmpty()) {
1841                 if (position == 0) {
1842                     return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
1843                 } else if (position == 1) {
1844                     return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS;
1845                 }
1846             } else {
1847                 if (position == 1) {
1848                     return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
1849                 }
1850                 if (position == getCount() - 1) {
1851                     return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS;
1852                 }
1853             }
1854             return position;
1855         }
1856
1857         @Override
1858         public View getDropDownView(int position, View convertView, ViewGroup parent) {
1859             View view = getView(position, convertView, parent);
1860             view.setEnabled(isEnabled(position));
1861             return view;
1862         }
1863
1864         @Override
1865         public View getView(int position, View convertView, ViewGroup parent) {
1866             if (convertView == null) {
1867                 convertView = getLayoutInflater().inflate(
1868                         R.layout.printer_dropdown_item, parent, false);
1869             }
1870
1871             CharSequence title = null;
1872             CharSequence subtitle = null;
1873             Drawable icon = null;
1874
1875             if (mPrinterHolders.isEmpty()) {
1876                 if (position == 0 && getPdfPrinter() != null) {
1877                     PrinterHolder printerHolder = (PrinterHolder) getItem(position);
1878                     title = printerHolder.printer.getName();
1879                     icon = getResources().getDrawable(R.drawable.ic_menu_savetopdf);
1880                 } else if (position == 1) {
1881                     title = getString(R.string.all_printers);
1882                 }
1883             } else {
1884                 if (position == 1 && getPdfPrinter() != null) {
1885                     PrinterHolder printerHolder = (PrinterHolder) getItem(position);
1886                     title = printerHolder.printer.getName();
1887                     icon = getResources().getDrawable(R.drawable.ic_menu_savetopdf);
1888                 } else if (position == getCount() - 1) {
1889                     title = getString(R.string.all_printers);
1890                 } else {
1891                     PrinterHolder printerHolder = (PrinterHolder) getItem(position);
1892                     title = printerHolder.printer.getName();
1893                     try {
1894                         PackageInfo packageInfo = getPackageManager().getPackageInfo(
1895                                 printerHolder.printer.getId().getServiceName().getPackageName(), 0);
1896                         subtitle = packageInfo.applicationInfo.loadLabel(getPackageManager());
1897                         icon = packageInfo.applicationInfo.loadIcon(getPackageManager());
1898                     } catch (NameNotFoundException nnfe) {
1899                         /* ignore */
1900                     }
1901                 }
1902             }
1903
1904             TextView titleView = (TextView) convertView.findViewById(R.id.title);
1905             titleView.setText(title);
1906
1907             TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle);
1908             if (!TextUtils.isEmpty(subtitle)) {
1909                 subtitleView.setText(subtitle);
1910                 subtitleView.setVisibility(View.VISIBLE);
1911             } else {
1912                 subtitleView.setText(null);
1913                 subtitleView.setVisibility(View.GONE);
1914             }
1915
1916             ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
1917             if (icon != null) {
1918                 iconView.setImageDrawable(icon);
1919                 iconView.setVisibility(View.VISIBLE);
1920             } else {
1921                 iconView.setVisibility(View.INVISIBLE);
1922             }
1923
1924             return convertView;
1925         }
1926
1927         @Override
1928         public void onPrintersChanged(List<PrinterInfo> printers) {
1929             // We rearrange the printers if the user selects a printer
1930             // not shown in the initial short list. Therefore, we have
1931             // to keep the printer order.
1932
1933             // Check if historical printers are loaded as this adapter is open
1934             // for busyness only if they are. This member is updated here and
1935             // when the adapter is created because the historical printers may
1936             // be loaded before or after the adapter is created.
1937             mHistoricalPrintersLoaded = mPrinterRegistry.areHistoricalPrintersLoaded();
1938
1939             // No old printers - do not bother keeping their position.
1940             if (mPrinterHolders.isEmpty()) {
1941                 addPrinters(mPrinterHolders, printers);
1942                 notifyDataSetChanged();
1943                 return;
1944             }
1945
1946             // Add the new printers to a map.
1947             ArrayMap<PrinterId, PrinterInfo> newPrintersMap = new ArrayMap<>();
1948             final int printerCount = printers.size();
1949             for (int i = 0; i < printerCount; i++) {
1950                 PrinterInfo printer = printers.get(i);
1951                 newPrintersMap.put(printer.getId(), printer);
1952             }
1953
1954             List<PrinterHolder> newPrinterHolders = new ArrayList<>();
1955
1956             // Update printers we already have which are either updated or removed.
1957             // We do not remove printers if the currently selected printer is removed
1958             // to prevent the user printing to a wrong printer.
1959             final int oldPrinterCount = mPrinterHolders.size();
1960             for (int i = 0; i < oldPrinterCount; i++) {
1961                 PrinterHolder printerHolder = mPrinterHolders.get(i);
1962                 PrinterId oldPrinterId = printerHolder.printer.getId();
1963                 PrinterInfo updatedPrinter = newPrintersMap.remove(oldPrinterId);
1964                 if (updatedPrinter != null) {
1965                     printerHolder.printer = updatedPrinter;
1966                 } else {
1967                     printerHolder.removed = true;
1968                 }
1969                 newPrinterHolders.add(printerHolder);
1970             }
1971
1972             // Add the rest of the new printers, i.e. what is left.
1973             addPrinters(newPrinterHolders, newPrintersMap.values());
1974
1975             mPrinterHolders.clear();
1976             mPrinterHolders.addAll(newPrinterHolders);
1977
1978             notifyDataSetChanged();
1979         }
1980
1981         @Override
1982         public void onPrintersInvalid() {
1983             mPrinterHolders.clear();
1984             notifyDataSetInvalidated();
1985         }
1986
1987         public PrinterHolder getPrinterHolder(PrinterId printerId) {
1988             final int itemCount = getCount();
1989             for (int i = 0; i < itemCount; i++) {
1990                 Object item = getItem(i);
1991                 if (item instanceof PrinterHolder) {
1992                     PrinterHolder printerHolder = (PrinterHolder) item;
1993                     if (printerId.equals(printerHolder.printer.getId())) {
1994                         return printerHolder;
1995                     }
1996                 }
1997             }
1998             return null;
1999         }
2000
2001         public void pruneRemovedPrinters() {
2002             final int holderCounts = mPrinterHolders.size();
2003             for (int i = holderCounts - 1; i >= 0; i--) {
2004                 PrinterHolder printerHolder = mPrinterHolders.get(i);
2005                 if (printerHolder.removed) {
2006                     mPrinterHolders.remove(i);
2007                 }
2008             }
2009         }
2010
2011         private void addPrinters(List<PrinterHolder> list, Collection<PrinterInfo> printers) {
2012             for (PrinterInfo printer : printers) {
2013                 PrinterHolder printerHolder = new PrinterHolder(printer);
2014                 list.add(printerHolder);
2015             }
2016         }
2017
2018         private PrinterInfo createFakePdfPrinter() {
2019             MediaSize defaultMediaSize = MediaSizeUtils.getDefault(PrintActivity.this);
2020
2021             PrinterId printerId = new PrinterId(getComponentName(), "PDF printer");
2022
2023             PrinterCapabilitiesInfo.Builder builder =
2024                     new PrinterCapabilitiesInfo.Builder(printerId);
2025
2026             String[] mediaSizeIds = getResources().getStringArray(R.array.pdf_printer_media_sizes);
2027             final int mediaSizeIdCount = mediaSizeIds.length;
2028             for (int i = 0; i < mediaSizeIdCount; i++) {
2029                 String id = mediaSizeIds[i];
2030                 MediaSize mediaSize = MediaSize.getStandardMediaSizeById(id);
2031                 builder.addMediaSize(mediaSize, mediaSize.equals(defaultMediaSize));
2032             }
2033
2034             builder.addResolution(new Resolution("PDF resolution", "PDF resolution", 300, 300),
2035                     true);
2036             builder.setColorModes(PrintAttributes.COLOR_MODE_COLOR
2037                     | PrintAttributes.COLOR_MODE_MONOCHROME, PrintAttributes.COLOR_MODE_COLOR);
2038
2039             return new PrinterInfo.Builder(printerId, getString(R.string.save_as_pdf),
2040                     PrinterInfo.STATUS_IDLE).setCapabilities(builder.build()).build();
2041         }
2042     }
2043
2044     private final class PrintersObserver extends DataSetObserver {
2045         @Override
2046         public void onChanged() {
2047             PrinterInfo oldPrinterState = mCurrentPrinter;
2048             if (oldPrinterState == null) {
2049                 return;
2050             }
2051
2052             PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
2053                     oldPrinterState.getId());
2054             if (printerHolder == null) {
2055                 return;
2056             }
2057             PrinterInfo newPrinterState = printerHolder.printer;
2058
2059             if (!printerHolder.removed) {
2060                 mDestinationSpinnerAdapter.pruneRemovedPrinters();
2061             } else {
2062                 onPrinterUnavailable(newPrinterState);
2063             }
2064
2065             if (oldPrinterState.equals(newPrinterState)) {
2066                 return;
2067             }
2068
2069             PrinterCapabilitiesInfo oldCapab = oldPrinterState.getCapabilities();
2070             PrinterCapabilitiesInfo newCapab = newPrinterState.getCapabilities();
2071
2072             final boolean hasCapab = newCapab != null;
2073             final boolean gotCapab = oldCapab == null && newCapab != null;
2074             final boolean lostCapab = oldCapab != null && newCapab == null;
2075             final boolean capabChanged = capabilitiesChanged(oldCapab, newCapab);
2076
2077             final int oldStatus = oldPrinterState.getStatus();
2078             final int newStatus = newPrinterState.getStatus();
2079
2080             final boolean isActive = newStatus != PrinterInfo.STATUS_UNAVAILABLE;
2081             final boolean becameActive = (oldStatus == PrinterInfo.STATUS_UNAVAILABLE
2082                     && oldStatus != newStatus);
2083             final boolean becameInactive = (newStatus == PrinterInfo.STATUS_UNAVAILABLE
2084                     && oldStatus != newStatus);
2085
2086             mPrinterAvailabilityDetector.updatePrinter(newPrinterState);
2087
2088             oldPrinterState.copyFrom(newPrinterState);
2089
2090             if ((isActive && gotCapab) || (becameActive && hasCapab)) {
2091                 if (hasCapab && capabChanged) {
2092                     updatePrintAttributesFromCapabilities(newCapab);
2093                     updatePrintPreviewController(false);
2094                 }
2095                 onPrinterAvailable(newPrinterState);
2096             } else if ((becameInactive && hasCapab) || (isActive && lostCapab)) {
2097                 onPrinterUnavailable(newPrinterState);
2098             }
2099
2100             final boolean updateNeeded = ((capabChanged && hasCapab && isActive)
2101                     || (becameActive && hasCapab) || (isActive && gotCapab));
2102
2103             if (updateNeeded && canUpdateDocument()) {
2104                 updateDocument(false);
2105             }
2106
2107             updateOptionsUi();
2108         }
2109
2110         private boolean capabilitiesChanged(PrinterCapabilitiesInfo oldCapabilities,
2111                 PrinterCapabilitiesInfo newCapabilities) {
2112             if (oldCapabilities == null) {
2113                 if (newCapabilities != null) {
2114                     return true;
2115                 }
2116             } else if (!oldCapabilities.equals(newCapabilities)) {
2117                 return true;
2118             }
2119             return false;
2120         }
2121     }
2122
2123     private final class MyOnItemSelectedListener implements AdapterView.OnItemSelectedListener {
2124         @Override
2125         public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) {
2126             if (spinner == mDestinationSpinner) {
2127                 if (position == AdapterView.INVALID_POSITION) {
2128                     return;
2129                 }
2130
2131                 if (id == DEST_ADAPTER_ITEM_ID_ALL_PRINTERS) {
2132                     startSelectPrinterActivity();
2133                     return;
2134                 }
2135
2136                 PrinterHolder currentItem = (PrinterHolder) mDestinationSpinner.getSelectedItem();
2137                 PrinterInfo currentPrinter = (currentItem != null) ? currentItem.printer : null;
2138
2139                 // Why on earth item selected is called if no selection changed.
2140                 if (mCurrentPrinter == currentPrinter) {
2141                     return;
2142                 }
2143
2144                 mCurrentPrinter = currentPrinter;
2145
2146                 PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
2147                         currentPrinter.getId());
2148                 if (!printerHolder.removed) {
2149                     setState(STATE_CONFIGURING);
2150                     mDestinationSpinnerAdapter.pruneRemovedPrinters();
2151                     ensurePreviewUiShown();
2152                 }
2153
2154                 mPrintJob.setPrinterId(currentPrinter.getId());
2155                 mPrintJob.setPrinterName(currentPrinter.getName());
2156
2157                 mPrinterRegistry.setTrackedPrinter(currentPrinter.getId());
2158
2159                 PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities();
2160                 if (capabilities != null) {
2161                     updatePrintAttributesFromCapabilities(capabilities);
2162                 }
2163
2164                 mPrinterAvailabilityDetector.updatePrinter(currentPrinter);
2165             } else if (spinner == mMediaSizeSpinner) {
2166                 SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position);
2167                 PrintAttributes attributes = mPrintJob.getAttributes();
2168                 if (mOrientationSpinner.getSelectedItemPosition() == 0) {
2169                     attributes.setMediaSize(mediaItem.value.asPortrait());
2170                 } else {
2171                     attributes.setMediaSize(mediaItem.value.asLandscape());
2172                 }
2173             } else if (spinner == mColorModeSpinner) {
2174                 SpinnerItem<Integer> colorModeItem = mColorModeSpinnerAdapter.getItem(position);
2175                 mPrintJob.getAttributes().setColorMode(colorModeItem.value);
2176             } else if (spinner == mOrientationSpinner) {
2177                 SpinnerItem<Integer> orientationItem = mOrientationSpinnerAdapter.getItem(position);
2178                 PrintAttributes attributes = mPrintJob.getAttributes();
2179                 if (mMediaSizeSpinner.getSelectedItem() != null) {
2180                     if (orientationItem.value == ORIENTATION_PORTRAIT) {
2181                         attributes.copyFrom(attributes.asPortrait());
2182                     } else {
2183                         attributes.copyFrom(attributes.asLandscape());
2184                     }
2185                 }
2186             } else if (spinner == mRangeOptionsSpinner) {
2187                 if (mRangeOptionsSpinner.getSelectedItemPosition() == 0) {
2188                     mPageRangeEditText.setText("");
2189                 } else if (TextUtils.isEmpty(mPageRangeEditText.getText())) {
2190                     mPageRangeEditText.setError("");
2191                 }
2192             }
2193
2194             if (canUpdateDocument()) {
2195                 updateDocument(false);
2196             }
2197
2198             updateOptionsUi();
2199         }
2200
2201         @Override
2202         public void onNothingSelected(AdapterView<?> parent) {
2203             /* do nothing*/
2204         }
2205     }
2206
2207     private final class SelectAllOnFocusListener implements OnFocusChangeListener {
2208         @Override
2209         public void onFocusChange(View view, boolean hasFocus) {
2210             EditText editText = (EditText) view;
2211             if (!TextUtils.isEmpty(editText.getText())) {
2212                 editText.setSelection(editText.getText().length());
2213             }
2214         }
2215     }
2216
2217     private final class RangeTextWatcher implements TextWatcher {
2218         @Override
2219         public void onTextChanged(CharSequence s, int start, int before, int count) {
2220             /* do nothing */
2221         }
2222
2223         @Override
2224         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2225             /* do nothing */
2226         }
2227
2228         @Override
2229         public void afterTextChanged(Editable editable) {
2230             final boolean hadErrors = hasErrors();
2231
2232             String text = editable.toString();
2233
2234             if (TextUtils.isEmpty(text)) {
2235                 mPageRangeEditText.setError("");
2236                 updateOptionsUi();
2237                 return;
2238             }
2239
2240             String escapedText = PATTERN_ESCAPE_SPECIAL_CHARS.matcher(text).replaceAll("////");
2241             if (!PATTERN_PAGE_RANGE.matcher(escapedText).matches()) {
2242                 mPageRangeEditText.setError("");
2243                 updateOptionsUi();
2244                 return;
2245             }
2246
2247             PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
2248             final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
2249
2250             // The range
2251             Matcher matcher = PATTERN_DIGITS.matcher(text);
2252             while (matcher.find()) {
2253                 String numericString = text.substring(matcher.start(), matcher.end()).trim();
2254                 if (TextUtils.isEmpty(numericString)) {
2255                     continue;
2256                 }
2257                 final int pageIndex = Integer.parseInt(numericString);
2258                 if (pageIndex < 1 || pageIndex > pageCount) {
2259                     mPageRangeEditText.setError("");
2260                     updateOptionsUi();
2261                     return;
2262                 }
2263             }
2264
2265             // We intentionally do not catch the case of the from page being
2266             // greater than the to page. When computing the requested pages
2267             // we just swap them if necessary.
2268
2269             mPageRangeEditText.setError(null);
2270             mPrintButton.setEnabled(true);
2271             updateOptionsUi();
2272
2273             if (hadErrors && !hasErrors()) {
2274                 updateOptionsUi();
2275             }
2276         }
2277     }
2278
2279     private final class EditTextWatcher implements TextWatcher {
2280         @Override
2281         public void onTextChanged(CharSequence s, int start, int before, int count) {
2282             /* do nothing */
2283         }
2284
2285         @Override
2286         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2287             /* do nothing */
2288         }
2289
2290         @Override
2291         public void afterTextChanged(Editable editable) {
2292             final boolean hadErrors = hasErrors();
2293
2294             if (editable.length() == 0) {
2295                 mCopiesEditText.setError("");
2296                 updateOptionsUi();
2297                 return;
2298             }
2299
2300             int copies = 0;
2301             try {
2302                 copies = Integer.parseInt(editable.toString());
2303             } catch (NumberFormatException nfe) {
2304                 /* ignore */
2305             }
2306
2307             if (copies < MIN_COPIES) {
2308                 mCopiesEditText.setError("");
2309                 updateOptionsUi();
2310                 return;
2311             }
2312
2313             mPrintJob.setCopies(copies);
2314
2315             mCopiesEditText.setError(null);
2316
2317             updateOptionsUi();
2318
2319             if (hadErrors && canUpdateDocument()) {
2320                 updateDocument(false);
2321             }
2322         }
2323     }
2324
2325     private final class ProgressMessageController implements Runnable {
2326         private static final long PROGRESS_TIMEOUT_MILLIS = 1000;
2327
2328         private final Handler mHandler;
2329
2330         private boolean mPosted;
2331
2332         public ProgressMessageController(Context context) {
2333             mHandler = new Handler(context.getMainLooper(), null, false);
2334         }
2335
2336         public void post() {
2337             if (mPosted) {
2338                 return;
2339             }
2340             mPosted = true;
2341             mHandler.postDelayed(this, PROGRESS_TIMEOUT_MILLIS);
2342         }
2343
2344         public void cancel() {
2345             if (!mPosted) {
2346                 return;
2347             }
2348             mPosted = false;
2349             mHandler.removeCallbacks(this);
2350         }
2351
2352         @Override
2353         public void run() {
2354             mPosted = false;
2355             setState(STATE_UPDATE_SLOW);
2356             ensureProgressUiShown();
2357             updateOptionsUi();
2358         }
2359     }
2360
2361     private static final class DocumentTransformer implements ServiceConnection {
2362         private static final String TEMP_FILE_PREFIX = "print_job";
2363         private static final String TEMP_FILE_EXTENSION = ".pdf";
2364
2365         private final Context mContext;
2366
2367         private final MutexFileProvider mFileProvider;
2368
2369         private final PrintJobInfo mPrintJob;
2370
2371         private final PageRange[] mPagesToShred;
2372
2373         private final PrintAttributes mAttributesToApply;
2374
2375         private final Runnable mCallback;
2376
2377         public DocumentTransformer(Context context, PrintJobInfo printJob,
2378                 MutexFileProvider fileProvider, PrintAttributes attributes,
2379                 Runnable callback) {
2380             mContext = context;
2381             mPrintJob = printJob;
2382             mFileProvider = fileProvider;
2383             mCallback = callback;
2384             mPagesToShred = computePagesToShred(mPrintJob);
2385             mAttributesToApply = attributes;
2386         }
2387
2388         public void transform() {
2389             // If we have only the pages we want, done.
2390             if (mPagesToShred.length <= 0 && mAttributesToApply == null) {
2391                 mCallback.run();
2392                 return;
2393             }
2394
2395             // Bind to the manipulation service and the work
2396             // will be performed upon connection to the service.
2397             Intent intent = new Intent(PdfManipulationService.ACTION_GET_EDITOR);
2398             intent.setClass(mContext, PdfManipulationService.class);
2399             mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
2400         }
2401
2402         @Override
2403         public void onServiceConnected(ComponentName name, IBinder service) {
2404             final IPdfEditor editor = IPdfEditor.Stub.asInterface(service);
2405             new AsyncTask<Void, Void, Void>() {
2406                 @Override
2407                 protected Void doInBackground(Void... params) {
2408                     // It's OK to access the data members as they are
2409                     // final and this code is the last one to touch
2410                     // them as shredding is the very last step, so the
2411                     // UI is not interactive at this point.
2412                     doTransform(editor);
2413                     updatePrintJob();
2414                     return null;
2415                 }
2416
2417                 @Override
2418                 protected void onPostExecute(Void aVoid) {
2419                     mContext.unbindService(DocumentTransformer.this);
2420                     mCallback.run();
2421                 }
2422             }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
2423         }
2424
2425         @Override
2426         public void onServiceDisconnected(ComponentName name) {
2427             /* do nothing */
2428         }
2429
2430         private void doTransform(IPdfEditor editor) {
2431             File tempFile = null;
2432             ParcelFileDescriptor src = null;
2433             ParcelFileDescriptor dst = null;
2434             InputStream in = null;
2435             OutputStream out = null;
2436             try {
2437                 File jobFile = mFileProvider.acquireFile(null);
2438                 src = ParcelFileDescriptor.open(jobFile, ParcelFileDescriptor.MODE_READ_WRITE);
2439
2440                 // Open the document.
2441                 editor.openDocument(src);
2442
2443                 // We passed the fd over IPC, close this one.
2444                 src.close();
2445
2446                 // Drop the pages.
2447                 editor.removePages(mPagesToShred);
2448
2449                 // Apply print attributes if needed.
2450                 if (mAttributesToApply != null) {
2451                     editor.applyPrintAttributes(mAttributesToApply);
2452                 }
2453
2454                 // Write the modified PDF to a temp file.
2455                 tempFile = File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_EXTENSION,
2456                         mContext.getCacheDir());
2457                 dst = ParcelFileDescriptor.open(tempFile, ParcelFileDescriptor.MODE_READ_WRITE);
2458                 editor.write(dst);
2459                 dst.close();
2460
2461                 // Close the document.
2462                 editor.closeDocument();
2463
2464                 // Copy the temp file over the print job file.
2465                 jobFile.delete();
2466                 in = new FileInputStream(tempFile);
2467                 out = new FileOutputStream(jobFile);
2468                 Streams.copy(in, out);
2469             } catch (IOException|RemoteException e) {
2470                 Log.e(LOG_TAG, "Error dropping pages", e);
2471             } finally {
2472                 IoUtils.closeQuietly(src);
2473                 IoUtils.closeQuietly(dst);
2474                 IoUtils.closeQuietly(in);
2475                 IoUtils.closeQuietly(out);
2476                 if (tempFile != null) {
2477                     tempFile.delete();
2478                 }
2479                 mFileProvider.releaseFile();
2480             }
2481         }
2482
2483         private void updatePrintJob() {
2484             // Update the print job pages.
2485             final int newPageCount = PageRangeUtils.getNormalizedPageCount(
2486                     mPrintJob.getPages(), 0);
2487             mPrintJob.setPages(new PageRange[]{PageRange.ALL_PAGES});
2488
2489             // Update the print job document info.
2490             PrintDocumentInfo oldDocInfo = mPrintJob.getDocumentInfo();
2491             PrintDocumentInfo newDocInfo = new PrintDocumentInfo
2492                     .Builder(oldDocInfo.getName())
2493                     .setContentType(oldDocInfo.getContentType())
2494                     .setPageCount(newPageCount)
2495                     .build();
2496             mPrintJob.setDocumentInfo(newDocInfo);
2497         }
2498
2499         private static PageRange[] computePagesToShred(PrintJobInfo printJob) {
2500             List<PageRange> rangesToShred = new ArrayList<>();
2501             PageRange previousRange = null;
2502
2503             final int pageCount = printJob.getDocumentInfo().getPageCount();
2504
2505             PageRange[] printedPages = printJob.getPages();
2506             final int rangeCount = printedPages.length;
2507             for (int i = 0; i < rangeCount; i++) {
2508                 PageRange range = PageRangeUtils.asAbsoluteRange(printedPages[i], pageCount);
2509
2510                 if (previousRange == null) {
2511                     final int startPageIdx = 0;
2512                     final int endPageIdx = range.getStart() - 1;
2513                     if (startPageIdx <= endPageIdx) {
2514                         PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
2515                         rangesToShred.add(removedRange);
2516                     }
2517                 } else {
2518                     final int startPageIdx = previousRange.getEnd() + 1;
2519                     final int endPageIdx = range.getStart() - 1;
2520                     if (startPageIdx <= endPageIdx) {
2521                         PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
2522                         rangesToShred.add(removedRange);
2523                     }
2524                 }
2525
2526                 if (i == rangeCount - 1) {
2527                     final int startPageIdx = range.getEnd() + 1;
2528                     final int endPageIdx = printJob.getDocumentInfo().getPageCount() - 1;
2529                     if (startPageIdx <= endPageIdx) {
2530                         PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
2531                         rangesToShred.add(removedRange);
2532                     }
2533                 }
2534
2535                 previousRange = range;
2536             }
2537
2538             PageRange[] result = new PageRange[rangesToShred.size()];
2539             rangesToShred.toArray(result);
2540             return result;
2541         }
2542     }
2543 }