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