OSDN Git Service

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