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.AlertDialog;
21 import android.app.Dialog;
22 import android.app.DialogFragment;
23 import android.app.Fragment;
24 import android.app.FragmentTransaction;
25 import android.app.LoaderManager;
26 import android.content.ActivityNotFoundException;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.content.DialogInterface;
30 import android.content.Intent;
31 import android.content.Loader;
32 import android.content.ServiceConnection;
33 import android.content.SharedPreferences;
34 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
35 import android.content.pm.PackageManager;
36 import android.content.pm.PackageManager.NameNotFoundException;
37 import android.content.pm.ResolveInfo;
38 import android.content.res.Configuration;
39 import android.database.DataSetObserver;
40 import android.graphics.drawable.Drawable;
41 import android.net.Uri;
42 import android.os.AsyncTask;
43 import android.os.Bundle;
44 import android.os.Handler;
45 import android.os.IBinder;
46 import android.os.ParcelFileDescriptor;
47 import android.os.RemoteException;
48 import android.print.IPrintDocumentAdapter;
49 import android.print.PageRange;
50 import android.print.PrintAttributes;
51 import android.print.PrintAttributes.MediaSize;
52 import android.print.PrintAttributes.Resolution;
53 import android.print.PrintDocumentInfo;
54 import android.print.PrintJobInfo;
55 import android.print.PrintManager;
56 import android.print.PrintServicesLoader;
57 import android.print.PrinterCapabilitiesInfo;
58 import android.print.PrinterId;
59 import android.print.PrinterInfo;
60 import android.printservice.PrintService;
61 import android.printservice.PrintServiceInfo;
62 import android.provider.DocumentsContract;
63 import android.text.Editable;
64 import android.text.TextUtils;
65 import android.text.TextWatcher;
66 import android.util.ArrayMap;
67 import android.util.ArraySet;
68 import android.util.Log;
69 import android.util.TypedValue;
70 import android.view.KeyEvent;
71 import android.view.MotionEvent;
72 import android.view.View;
73 import android.view.View.OnClickListener;
74 import android.view.View.OnFocusChangeListener;
75 import android.view.ViewGroup;
76 import android.view.inputmethod.InputMethodManager;
77 import android.widget.AdapterView;
78 import android.widget.AdapterView.OnItemSelectedListener;
79 import android.widget.ArrayAdapter;
80 import android.widget.BaseAdapter;
81 import android.widget.Button;
82 import android.widget.EditText;
83 import android.widget.ImageView;
84 import android.widget.Spinner;
85 import android.widget.TextView;
86 
87 import android.widget.Toast;
88 import com.android.internal.logging.MetricsLogger;
89 import com.android.printspooler.R;
90 import com.android.printspooler.model.MutexFileProvider;
91 import com.android.printspooler.model.PrintSpoolerProvider;
92 import com.android.printspooler.model.PrintSpoolerService;
93 import com.android.printspooler.model.RemotePrintDocument;
94 import com.android.printspooler.model.RemotePrintDocument.RemotePrintDocumentInfo;
95 import com.android.printspooler.renderer.IPdfEditor;
96 import com.android.printspooler.renderer.PdfManipulationService;
97 import com.android.printspooler.util.ApprovedPrintServices;
98 import com.android.printspooler.util.MediaSizeUtils;
99 import com.android.printspooler.util.MediaSizeUtils.MediaSizeComparator;
100 import com.android.printspooler.util.PageRangeUtils;
101 import com.android.printspooler.widget.PrintContentView;
102 import com.android.printspooler.widget.PrintContentView.OptionsStateChangeListener;
103 import com.android.printspooler.widget.PrintContentView.OptionsStateController;
104 
105 import libcore.io.IoUtils;
106 import libcore.io.Streams;
107 
108 import java.io.File;
109 import java.io.FileInputStream;
110 import java.io.FileOutputStream;
111 import java.io.IOException;
112 import java.io.InputStream;
113 import java.io.OutputStream;
114 import java.util.ArrayList;
115 import java.util.Arrays;
116 import java.util.Collection;
117 import java.util.Collections;
118 import java.util.List;
119 import java.util.Objects;
120 
121 public class PrintActivity extends Activity implements RemotePrintDocument.UpdateResultCallbacks,
122         PrintErrorFragment.OnActionListener, PageAdapter.ContentCallbacks,
123         OptionsStateChangeListener, OptionsStateController,
124         LoaderManager.LoaderCallbacks<List<PrintServiceInfo>> {
125     private static final String LOG_TAG = "PrintActivity";
126 
127     private static final boolean DEBUG = false;
128 
129     private static final String FRAGMENT_TAG = "FRAGMENT_TAG";
130 
131     private static final String HAS_PRINTED_PREF = "has_printed";
132 
133     private static final int LOADER_ID_ENABLED_PRINT_SERVICES = 1;
134     private static final int LOADER_ID_PRINT_REGISTRY = 2;
135     private static final int LOADER_ID_PRINT_REGISTRY_INT = 3;
136 
137     private static final int ORIENTATION_PORTRAIT = 0;
138     private static final int ORIENTATION_LANDSCAPE = 1;
139 
140     private static final int ACTIVITY_REQUEST_CREATE_FILE = 1;
141     private static final int ACTIVITY_REQUEST_SELECT_PRINTER = 2;
142     private static final int ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS = 3;
143 
144     private static final int DEST_ADAPTER_MAX_ITEM_COUNT = 9;
145 
146     private static final int DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF = Integer.MAX_VALUE;
147     private static final int DEST_ADAPTER_ITEM_ID_MORE = Integer.MAX_VALUE - 1;
148 
149     private static final int STATE_INITIALIZING = 0;
150     private static final int STATE_CONFIGURING = 1;
151     private static final int STATE_PRINT_CONFIRMED = 2;
152     private static final int STATE_PRINT_CANCELED = 3;
153     private static final int STATE_UPDATE_FAILED = 4;
154     private static final int STATE_CREATE_FILE_FAILED = 5;
155     private static final int STATE_PRINTER_UNAVAILABLE = 6;
156     private static final int STATE_UPDATE_SLOW = 7;
157     private static final int STATE_PRINT_COMPLETED = 8;
158 
159     private static final int UI_STATE_PREVIEW = 0;
160     private static final int UI_STATE_ERROR = 1;
161     private static final int UI_STATE_PROGRESS = 2;
162 
163     private static final int MIN_COPIES = 1;
164     private static final String MIN_COPIES_STRING = String.valueOf(MIN_COPIES);
165 
166     private boolean mIsOptionsUiBound = false;
167 
168     private final PrinterAvailabilityDetector mPrinterAvailabilityDetector =
169             new PrinterAvailabilityDetector();
170 
171     private final OnFocusChangeListener mSelectAllOnFocusListener = new SelectAllOnFocusListener();
172 
173     private PrintSpoolerProvider mSpoolerProvider;
174 
175     private PrintPreviewController mPrintPreviewController;
176 
177     private PrintJobInfo mPrintJob;
178     private RemotePrintDocument mPrintedDocument;
179     private PrinterRegistry mPrinterRegistry;
180 
181     private EditText mCopiesEditText;
182 
183     private TextView mPageRangeTitle;
184     private EditText mPageRangeEditText;
185 
186     private Spinner mDestinationSpinner;
187     private DestinationAdapter mDestinationSpinnerAdapter;
188     private boolean mShowDestinationPrompt;
189 
190     private Spinner mMediaSizeSpinner;
191     private ArrayAdapter<SpinnerItem<MediaSize>> mMediaSizeSpinnerAdapter;
192 
193     private Spinner mColorModeSpinner;
194     private ArrayAdapter<SpinnerItem<Integer>> mColorModeSpinnerAdapter;
195 
196     private Spinner mDuplexModeSpinner;
197     private ArrayAdapter<SpinnerItem<Integer>> mDuplexModeSpinnerAdapter;
198 
199     private Spinner mOrientationSpinner;
200     private ArrayAdapter<SpinnerItem<Integer>> mOrientationSpinnerAdapter;
201 
202     private Spinner mRangeOptionsSpinner;
203 
204     private PrintContentView mOptionsContent;
205 
206     private View mSummaryContainer;
207     private TextView mSummaryCopies;
208     private TextView mSummaryPaperSize;
209 
210     private Button mMoreOptionsButton;
211 
212     private ImageView mPrintButton;
213 
214     private ProgressMessageController mProgressMessageController;
215     private MutexFileProvider mFileProvider;
216 
217     private MediaSizeComparator mMediaSizeComparator;
218 
219     private PrinterInfo mCurrentPrinter;
220 
221     private PageRange[] mSelectedPages;
222 
223     private String mCallingPackageName;
224 
225     private int mCurrentPageCount;
226 
227     private int mState = STATE_INITIALIZING;
228 
229     private int mUiState = UI_STATE_PREVIEW;
230 
231     /** Observer for changes to the printers */
232     private PrintersObserver mPrintersObserver;
233 
234     /** Advances options activity name for current printer */
235     private ComponentName mAdvancedPrintOptionsActivity;
236 
237     /** Whether at least one print services is enabled or not */
238     private boolean mArePrintServicesEnabled;
239 
240     /** Is doFinish() already in progress */
241     private boolean mIsFinishing;
242 
243     @Override
onCreate(Bundle savedInstanceState)244     public void onCreate(Bundle savedInstanceState) {
245         super.onCreate(savedInstanceState);
246 
247         Bundle extras = getIntent().getExtras();
248 
249         mPrintJob = extras.getParcelable(PrintManager.EXTRA_PRINT_JOB);
250         if (mPrintJob == null) {
251             throw new IllegalArgumentException(PrintManager.EXTRA_PRINT_JOB
252                     + " cannot be null");
253         }
254         if (mPrintJob.getAttributes() == null) {
255             mPrintJob.setAttributes(new PrintAttributes.Builder().build());
256         }
257 
258         final IBinder adapter = extras.getBinder(PrintManager.EXTRA_PRINT_DOCUMENT_ADAPTER);
259         if (adapter == null) {
260             throw new IllegalArgumentException(PrintManager.EXTRA_PRINT_DOCUMENT_ADAPTER
261                     + " cannot be null");
262         }
263 
264         mCallingPackageName = extras.getString(DocumentsContract.EXTRA_PACKAGE_NAME);
265 
266         // This will take just a few milliseconds, so just wait to
267         // bind to the local service before showing the UI.
268         mSpoolerProvider = new PrintSpoolerProvider(this,
269                 new Runnable() {
270             @Override
271             public void run() {
272                 if (isFinishing() || isDestroyed()) {
273                     // onPause might have not been able to cancel the job, see PrintActivity#onPause
274                     // To be sure, cancel the job again. Double canceling does no harm.
275                     mSpoolerProvider.getSpooler().setPrintJobState(mPrintJob.getId(),
276                             PrintJobInfo.STATE_CANCELED, null);
277                 } else {
278                     onConnectedToPrintSpooler(adapter);
279                 }
280             }
281         });
282 
283         getLoaderManager().initLoader(LOADER_ID_ENABLED_PRINT_SERVICES, null, this);
284     }
285 
onConnectedToPrintSpooler(final IBinder documentAdapter)286     private void onConnectedToPrintSpooler(final IBinder documentAdapter) {
287         // Now that we are bound to the print spooler service,
288         // create the printer registry and wait for it to get
289         // the first batch of results which will be delivered
290         // after reading historical data. This should be pretty
291         // fast, so just wait before showing the UI.
292         mPrinterRegistry = new PrinterRegistry(PrintActivity.this, () -> {
293             (new Handler(getMainLooper())).post(() -> onPrinterRegistryReady(documentAdapter));
294         }, LOADER_ID_PRINT_REGISTRY, LOADER_ID_PRINT_REGISTRY_INT);
295     }
296 
onPrinterRegistryReady(IBinder documentAdapter)297     private void onPrinterRegistryReady(IBinder documentAdapter) {
298         // Now that we are bound to the local print spooler service
299         // and the printer registry loaded the historical printers
300         // we can show the UI without flickering.
301         setTitle(R.string.print_dialog);
302         setContentView(R.layout.print_activity);
303 
304         try {
305             mFileProvider = new MutexFileProvider(
306                     PrintSpoolerService.generateFileForPrintJob(
307                             PrintActivity.this, mPrintJob.getId()));
308         } catch (IOException ioe) {
309             // At this point we cannot recover, so just take it down.
310             throw new IllegalStateException("Cannot create print job file", ioe);
311         }
312 
313         mPrintPreviewController = new PrintPreviewController(PrintActivity.this,
314                 mFileProvider);
315         mPrintedDocument = new RemotePrintDocument(PrintActivity.this,
316                 IPrintDocumentAdapter.Stub.asInterface(documentAdapter),
317                 mFileProvider, new RemotePrintDocument.RemoteAdapterDeathObserver() {
318             @Override
319             public void onDied() {
320                 Log.w(LOG_TAG, "Printing app died unexpectedly");
321 
322                 // If we are finishing or we are in a state that we do not need any
323                 // data from the printing app, then no need to finish.
324                 if (isFinishing() || isDestroyed() ||
325                         (isFinalState(mState) && !mPrintedDocument.isUpdating())) {
326                     return;
327                 }
328                 setState(STATE_PRINT_CANCELED);
329                 mPrintedDocument.cancel(true);
330                 doFinish();
331             }
332         }, PrintActivity.this);
333         mProgressMessageController = new ProgressMessageController(
334                 PrintActivity.this);
335         mMediaSizeComparator = new MediaSizeComparator(PrintActivity.this);
336         mDestinationSpinnerAdapter = new DestinationAdapter();
337 
338         bindUi();
339         updateOptionsUi();
340 
341         // Now show the updated UI to avoid flicker.
342         mOptionsContent.setVisibility(View.VISIBLE);
343         mSelectedPages = computeSelectedPages();
344         mPrintedDocument.start();
345 
346         ensurePreviewUiShown();
347 
348         setState(STATE_CONFIGURING);
349     }
350 
351     @Override
onStart()352     public void onStart() {
353         super.onStart();
354         if (mPrinterRegistry != null && mCurrentPrinter != null) {
355             mPrinterRegistry.setTrackedPrinter(mCurrentPrinter.getId());
356         }
357         MetricsLogger.count(this, "print_preview", 1);
358     }
359 
360     @Override
onPause()361     public void onPause() {
362         PrintSpoolerService spooler = mSpoolerProvider.getSpooler();
363 
364         if (mState == STATE_INITIALIZING) {
365             if (isFinishing()) {
366                 if (spooler != null) {
367                     spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_CANCELED, null);
368                 }
369             }
370             super.onPause();
371             return;
372         }
373 
374         if (isFinishing()) {
375             spooler.updatePrintJobUserConfigurableOptionsNoPersistence(mPrintJob);
376 
377             switch (mState) {
378                 case STATE_PRINT_COMPLETED: {
379                     if (mCurrentPrinter == mDestinationSpinnerAdapter.getPdfPrinter()) {
380                         spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_COMPLETED,
381                                 null);
382                     } else {
383                         spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_QUEUED,
384                                 null);
385                     }
386                 } break;
387 
388                 case STATE_CREATE_FILE_FAILED: {
389                     spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_FAILED,
390                             getString(R.string.print_write_error_message));
391                 } break;
392 
393                 default: {
394                     spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_CANCELED, null);
395                 } break;
396             }
397         }
398 
399         super.onPause();
400     }
401 
402     @Override
onStop()403     protected void onStop() {
404         mPrinterAvailabilityDetector.cancel();
405 
406         if (mPrinterRegistry != null) {
407             mPrinterRegistry.setTrackedPrinter(null);
408         }
409 
410         super.onStop();
411     }
412 
413     @Override
onKeyDown(int keyCode, KeyEvent event)414     public boolean onKeyDown(int keyCode, KeyEvent event) {
415         if (keyCode == KeyEvent.KEYCODE_BACK) {
416             event.startTracking();
417             return true;
418         }
419         return super.onKeyDown(keyCode, event);
420     }
421 
422     @Override
onKeyUp(int keyCode, KeyEvent event)423     public boolean onKeyUp(int keyCode, KeyEvent event) {
424         if (mState == STATE_INITIALIZING) {
425             doFinish();
426             return true;
427         }
428 
429         if (mState == STATE_PRINT_CANCELED || mState == STATE_PRINT_CONFIRMED
430                 || mState == STATE_PRINT_COMPLETED) {
431             return true;
432         }
433 
434         if (keyCode == KeyEvent.KEYCODE_BACK
435                 && event.isTracking() && !event.isCanceled()) {
436             if (mPrintPreviewController != null && mPrintPreviewController.isOptionsOpened()
437                     && !hasErrors()) {
438                 mPrintPreviewController.closeOptions();
439             } else {
440                 cancelPrint();
441             }
442             return true;
443         }
444         return super.onKeyUp(keyCode, event);
445     }
446 
447     @Override
onRequestContentUpdate()448     public void onRequestContentUpdate() {
449         if (canUpdateDocument()) {
450             updateDocument(false);
451         }
452     }
453 
454     @Override
onMalformedPdfFile()455     public void onMalformedPdfFile() {
456         onPrintDocumentError("Cannot print a malformed PDF file");
457     }
458 
459     @Override
onSecurePdfFile()460     public void onSecurePdfFile() {
461         onPrintDocumentError("Cannot print a password protected PDF file");
462     }
463 
onPrintDocumentError(String message)464     private void onPrintDocumentError(String message) {
465         setState(mProgressMessageController.cancel());
466         ensureErrorUiShown(null, PrintErrorFragment.ACTION_RETRY);
467 
468         setState(STATE_UPDATE_FAILED);
469 
470         updateOptionsUi();
471 
472         mPrintedDocument.kill(message);
473     }
474 
475     @Override
onActionPerformed()476     public void onActionPerformed() {
477         if (mState == STATE_UPDATE_FAILED
478                 && canUpdateDocument() && updateDocument(true)) {
479             ensurePreviewUiShown();
480             setState(STATE_CONFIGURING);
481             updateOptionsUi();
482         }
483     }
484 
485     @Override
onUpdateCanceled()486     public void onUpdateCanceled() {
487         if (DEBUG) {
488             Log.i(LOG_TAG, "onUpdateCanceled()");
489         }
490 
491         setState(mProgressMessageController.cancel());
492         ensurePreviewUiShown();
493 
494         switch (mState) {
495             case STATE_PRINT_CONFIRMED: {
496                 requestCreatePdfFileOrFinish();
497             } break;
498 
499             case STATE_CREATE_FILE_FAILED:
500             case STATE_PRINT_COMPLETED:
501             case STATE_PRINT_CANCELED: {
502                 doFinish();
503             } break;
504         }
505     }
506 
507     @Override
onUpdateCompleted(RemotePrintDocumentInfo document)508     public void onUpdateCompleted(RemotePrintDocumentInfo document) {
509         if (DEBUG) {
510             Log.i(LOG_TAG, "onUpdateCompleted()");
511         }
512 
513         setState(mProgressMessageController.cancel());
514         ensurePreviewUiShown();
515 
516         // Update the print job with the info for the written document. The page
517         // count we get from the remote document is the pages in the document from
518         // the app perspective but the print job should contain the page count from
519         // print service perspective which is the pages in the written PDF not the
520         // pages in the printed document.
521         PrintDocumentInfo info = document.info;
522         if (info != null) {
523             final int pageCount = PageRangeUtils.getNormalizedPageCount(document.writtenPages,
524                     getAdjustedPageCount(info));
525             PrintDocumentInfo adjustedInfo = new PrintDocumentInfo.Builder(info.getName())
526                     .setContentType(info.getContentType())
527                     .setPageCount(pageCount)
528                     .build();
529             mPrintJob.setDocumentInfo(adjustedInfo);
530             mPrintJob.setPages(document.printedPages);
531         }
532 
533         switch (mState) {
534             case STATE_PRINT_CONFIRMED: {
535                 requestCreatePdfFileOrFinish();
536             } break;
537 
538             case STATE_CREATE_FILE_FAILED:
539             case STATE_PRINT_COMPLETED:
540             case STATE_PRINT_CANCELED: {
541                 updateOptionsUi();
542 
543                 doFinish();
544             } break;
545 
546             default: {
547                 updatePrintPreviewController(document.changed);
548 
549                 setState(STATE_CONFIGURING);
550                 updateOptionsUi();
551             } break;
552         }
553     }
554 
555     @Override
onUpdateFailed(CharSequence error)556     public void onUpdateFailed(CharSequence error) {
557         if (DEBUG) {
558             Log.i(LOG_TAG, "onUpdateFailed()");
559         }
560 
561         setState(mProgressMessageController.cancel());
562         ensureErrorUiShown(error, PrintErrorFragment.ACTION_RETRY);
563 
564         if (mState == STATE_CREATE_FILE_FAILED
565                 || mState == STATE_PRINT_COMPLETED
566                 || mState == STATE_PRINT_CANCELED) {
567             doFinish();
568         }
569 
570         setState(STATE_UPDATE_FAILED);
571 
572         updateOptionsUi();
573     }
574 
575     @Override
onOptionsOpened()576     public void onOptionsOpened() {
577         updateSelectedPagesFromPreview();
578     }
579 
580     @Override
onOptionsClosed()581     public void onOptionsClosed() {
582         // Make sure the IME is not on the way of preview as
583         // the user may have used it to type copies or range.
584         InputMethodManager imm = getSystemService(InputMethodManager.class);
585         imm.hideSoftInputFromWindow(mDestinationSpinner.getWindowToken(), 0);
586     }
587 
updatePrintPreviewController(boolean contentUpdated)588     private void updatePrintPreviewController(boolean contentUpdated) {
589         // If we have not heard from the application, do nothing.
590         RemotePrintDocumentInfo documentInfo = mPrintedDocument.getDocumentInfo();
591         if (!documentInfo.laidout) {
592             return;
593         }
594 
595         // Update the preview controller.
596         mPrintPreviewController.onContentUpdated(contentUpdated,
597                 getAdjustedPageCount(documentInfo.info),
598                 mPrintedDocument.getDocumentInfo().writtenPages,
599                 mSelectedPages, mPrintJob.getAttributes().getMediaSize(),
600                 mPrintJob.getAttributes().getMinMargins());
601     }
602 
603 
604     @Override
canOpenOptions()605     public boolean canOpenOptions() {
606         return true;
607     }
608 
609     @Override
canCloseOptions()610     public boolean canCloseOptions() {
611         return !hasErrors();
612     }
613 
614     @Override
onConfigurationChanged(Configuration newConfig)615     public void onConfigurationChanged(Configuration newConfig) {
616         super.onConfigurationChanged(newConfig);
617 
618         mMediaSizeComparator.onConfigurationChanged(newConfig);
619 
620         if (mPrintPreviewController != null) {
621             mPrintPreviewController.onOrientationChanged();
622         }
623     }
624 
625     @Override
onDestroy()626     protected void onDestroy() {
627         if (mPrintedDocument != null) {
628             mPrintedDocument.cancel(true);
629         }
630 
631         doFinish();
632 
633         super.onDestroy();
634     }
635 
636     @Override
onActivityResult(int requestCode, int resultCode, Intent data)637     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
638         switch (requestCode) {
639             case ACTIVITY_REQUEST_CREATE_FILE: {
640                 onStartCreateDocumentActivityResult(resultCode, data);
641             } break;
642 
643             case ACTIVITY_REQUEST_SELECT_PRINTER: {
644                 onSelectPrinterActivityResult(resultCode, data);
645             } break;
646 
647             case ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS: {
648                 onAdvancedPrintOptionsActivityResult(resultCode, data);
649             } break;
650         }
651     }
652 
startCreateDocumentActivity()653     private void startCreateDocumentActivity() {
654         if (!isResumed()) {
655             return;
656         }
657         PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
658         if (info == null) {
659             return;
660         }
661         Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
662         intent.setType("application/pdf");
663         intent.putExtra(Intent.EXTRA_TITLE, info.getName());
664         intent.putExtra(DocumentsContract.EXTRA_PACKAGE_NAME, mCallingPackageName);
665 
666         try {
667             startActivityForResult(intent, ACTIVITY_REQUEST_CREATE_FILE);
668         } catch (Exception e) {
669             Log.e(LOG_TAG, "Could not create file", e);
670             Toast.makeText(this, getString(R.string.could_not_create_file),
671                     Toast.LENGTH_SHORT).show();
672             onStartCreateDocumentActivityResult(RESULT_CANCELED, null);
673         }
674     }
675 
onStartCreateDocumentActivityResult(int resultCode, Intent data)676     private void onStartCreateDocumentActivityResult(int resultCode, Intent data) {
677         if (resultCode == RESULT_OK && data != null) {
678             updateOptionsUi();
679             final Uri uri = data.getData();
680             // Calling finish here does not invoke lifecycle callbacks but we
681             // update the print job in onPause if finishing, hence post a message.
682             mDestinationSpinner.post(new Runnable() {
683                 @Override
684                 public void run() {
685                     transformDocumentAndFinish(uri);
686                 }
687             });
688         } else if (resultCode == RESULT_CANCELED) {
689             if (DEBUG) {
690                 Log.i(LOG_TAG, "[state]" + STATE_CONFIGURING);
691             }
692 
693             mState = STATE_CONFIGURING;
694 
695             // The previous update might have been canceled
696             updateDocument(false);
697 
698             updateOptionsUi();
699         } else {
700             setState(STATE_CREATE_FILE_FAILED);
701             updateOptionsUi();
702             // Calling finish here does not invoke lifecycle callbacks but we
703             // update the print job in onPause if finishing, hence post a message.
704             mDestinationSpinner.post(new Runnable() {
705                 @Override
706                 public void run() {
707                     doFinish();
708                 }
709             });
710         }
711     }
712 
startSelectPrinterActivity()713     private void startSelectPrinterActivity() {
714         Intent intent = new Intent(this, SelectPrinterActivity.class);
715         startActivityForResult(intent, ACTIVITY_REQUEST_SELECT_PRINTER);
716     }
717 
onSelectPrinterActivityResult(int resultCode, Intent data)718     private void onSelectPrinterActivityResult(int resultCode, Intent data) {
719         if (resultCode == RESULT_OK && data != null) {
720             PrinterInfo printerInfo = data.getParcelableExtra(
721                     SelectPrinterActivity.INTENT_EXTRA_PRINTER);
722             if (printerInfo != null) {
723                 mCurrentPrinter = printerInfo;
724                 mPrintJob.setPrinterId(printerInfo.getId());
725                 mPrintJob.setPrinterName(printerInfo.getName());
726 
727                 mDestinationSpinnerAdapter.ensurePrinterInVisibleAdapterPosition(printerInfo);
728             }
729         }
730 
731         if (mCurrentPrinter != null) {
732             // Trigger PrintersObserver.onChanged() to adjust selection back to current printer
733             mDestinationSpinnerAdapter.notifyDataSetChanged();
734         }
735     }
736 
startAdvancedPrintOptionsActivity(PrinterInfo printer)737     private void startAdvancedPrintOptionsActivity(PrinterInfo printer) {
738         if (mAdvancedPrintOptionsActivity == null) {
739             return;
740         }
741 
742         Intent intent = new Intent(Intent.ACTION_MAIN);
743         intent.setComponent(mAdvancedPrintOptionsActivity);
744 
745         List<ResolveInfo> resolvedActivities = getPackageManager()
746                 .queryIntentActivities(intent, 0);
747         if (resolvedActivities.isEmpty()) {
748             return;
749         }
750 
751         // The activity is a component name, therefore it is one or none.
752         if (resolvedActivities.get(0).activityInfo.exported) {
753             PrintJobInfo.Builder printJobBuilder = new PrintJobInfo.Builder(mPrintJob);
754             printJobBuilder.setPages(mSelectedPages);
755 
756             intent.putExtra(PrintService.EXTRA_PRINT_JOB_INFO, printJobBuilder.build());
757             intent.putExtra(PrintService.EXTRA_PRINTER_INFO, printer);
758             intent.putExtra(PrintService.EXTRA_PRINT_DOCUMENT_INFO,
759                     mPrintedDocument.getDocumentInfo().info);
760 
761             // This is external activity and may not be there.
762             try {
763                 startActivityForResult(intent, ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS);
764             } catch (ActivityNotFoundException anfe) {
765                 Log.e(LOG_TAG, "Error starting activity for intent: " + intent, anfe);
766             }
767         }
768     }
769 
onAdvancedPrintOptionsActivityResult(int resultCode, Intent data)770     private void onAdvancedPrintOptionsActivityResult(int resultCode, Intent data) {
771         if (resultCode != RESULT_OK || data == null) {
772             return;
773         }
774 
775         PrintJobInfo printJobInfo = data.getParcelableExtra(PrintService.EXTRA_PRINT_JOB_INFO);
776 
777         if (printJobInfo == null) {
778             return;
779         }
780 
781         // Take the advanced options without interpretation.
782         mPrintJob.setAdvancedOptions(printJobInfo.getAdvancedOptions());
783 
784         if (printJobInfo.getCopies() < 1) {
785             Log.w(LOG_TAG, "Cannot apply return value from advanced options activity. Copies " +
786                     "must be 1 or more. Actual value is: " + printJobInfo.getCopies() + ". " +
787                     "Ignoring.");
788         } else {
789             mCopiesEditText.setText(String.valueOf(printJobInfo.getCopies()));
790             mPrintJob.setCopies(printJobInfo.getCopies());
791         }
792 
793         PrintAttributes currAttributes = mPrintJob.getAttributes();
794         PrintAttributes newAttributes = printJobInfo.getAttributes();
795 
796         if (newAttributes != null) {
797             // Take the media size only if the current printer supports is.
798             MediaSize oldMediaSize = currAttributes.getMediaSize();
799             MediaSize newMediaSize = newAttributes.getMediaSize();
800             if (newMediaSize != null && !oldMediaSize.equals(newMediaSize)) {
801                 final int mediaSizeCount = mMediaSizeSpinnerAdapter.getCount();
802                 MediaSize newMediaSizePortrait = newAttributes.getMediaSize().asPortrait();
803                 for (int i = 0; i < mediaSizeCount; i++) {
804                     MediaSize supportedSizePortrait = mMediaSizeSpinnerAdapter.getItem(i)
805                             .value.asPortrait();
806                     if (supportedSizePortrait.equals(newMediaSizePortrait)) {
807                         currAttributes.setMediaSize(newMediaSize);
808                         mMediaSizeSpinner.setSelection(i);
809                         if (currAttributes.getMediaSize().isPortrait()) {
810                             if (mOrientationSpinner.getSelectedItemPosition() != 0) {
811                                 mOrientationSpinner.setSelection(0);
812                             }
813                         } else {
814                             if (mOrientationSpinner.getSelectedItemPosition() != 1) {
815                                 mOrientationSpinner.setSelection(1);
816                             }
817                         }
818                         break;
819                     }
820                 }
821             }
822 
823             // Take the resolution only if the current printer supports is.
824             Resolution oldResolution = currAttributes.getResolution();
825             Resolution newResolution = newAttributes.getResolution();
826             if (!oldResolution.equals(newResolution)) {
827                 PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
828                 if (capabilities != null) {
829                     List<Resolution> resolutions = capabilities.getResolutions();
830                     final int resolutionCount = resolutions.size();
831                     for (int i = 0; i < resolutionCount; i++) {
832                         Resolution resolution = resolutions.get(i);
833                         if (resolution.equals(newResolution)) {
834                             currAttributes.setResolution(resolution);
835                             break;
836                         }
837                     }
838                 }
839             }
840 
841             // Take the color mode only if the current printer supports it.
842             final int currColorMode = currAttributes.getColorMode();
843             final int newColorMode = newAttributes.getColorMode();
844             if (currColorMode != newColorMode) {
845                 final int colorModeCount = mColorModeSpinner.getCount();
846                 for (int i = 0; i < colorModeCount; i++) {
847                     final int supportedColorMode = mColorModeSpinnerAdapter.getItem(i).value;
848                     if (supportedColorMode == newColorMode) {
849                         currAttributes.setColorMode(newColorMode);
850                         mColorModeSpinner.setSelection(i);
851                         break;
852                     }
853                 }
854             }
855 
856             // Take the duplex mode only if the current printer supports it.
857             final int currDuplexMode = currAttributes.getDuplexMode();
858             final int newDuplexMode = newAttributes.getDuplexMode();
859             if (currDuplexMode != newDuplexMode) {
860                 final int duplexModeCount = mDuplexModeSpinner.getCount();
861                 for (int i = 0; i < duplexModeCount; i++) {
862                     final int supportedDuplexMode = mDuplexModeSpinnerAdapter.getItem(i).value;
863                     if (supportedDuplexMode == newDuplexMode) {
864                         currAttributes.setDuplexMode(newDuplexMode);
865                         mDuplexModeSpinner.setSelection(i);
866                         break;
867                     }
868                 }
869             }
870         }
871 
872         // Handle selected page changes making sure they are in the doc.
873         PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
874         final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
875         PageRange[] pageRanges = printJobInfo.getPages();
876         if (pageRanges != null && pageCount > 0) {
877             pageRanges = PageRangeUtils.normalize(pageRanges);
878 
879             List<PageRange> validatedList = new ArrayList<>();
880             final int rangeCount = pageRanges.length;
881             for (int i = 0; i < rangeCount; i++) {
882                 PageRange pageRange = pageRanges[i];
883                 if (pageRange.getEnd() >= pageCount) {
884                     final int rangeStart = pageRange.getStart();
885                     final int rangeEnd = pageCount - 1;
886                     if (rangeStart <= rangeEnd) {
887                         pageRange = new PageRange(rangeStart, rangeEnd);
888                         validatedList.add(pageRange);
889                     }
890                     break;
891                 }
892                 validatedList.add(pageRange);
893             }
894 
895             if (!validatedList.isEmpty()) {
896                 PageRange[] validatedArray = new PageRange[validatedList.size()];
897                 validatedList.toArray(validatedArray);
898                 updateSelectedPages(validatedArray, pageCount);
899             }
900         }
901 
902         // Update the content if needed.
903         if (canUpdateDocument()) {
904             updateDocument(false);
905         }
906     }
907 
setState(int state)908     private void setState(int state) {
909         if (isFinalState(mState)) {
910             if (isFinalState(state)) {
911                 if (DEBUG) {
912                     Log.i(LOG_TAG, "[state]" + state);
913                 }
914                 mState = state;
915             }
916         } else {
917             if (DEBUG) {
918                 Log.i(LOG_TAG, "[state]" + state);
919             }
920             mState = state;
921         }
922     }
923 
isFinalState(int state)924     private static boolean isFinalState(int state) {
925         return state == STATE_PRINT_CANCELED
926                 || state == STATE_PRINT_COMPLETED
927                 || state == STATE_CREATE_FILE_FAILED;
928     }
929 
updateSelectedPagesFromPreview()930     private void updateSelectedPagesFromPreview() {
931         PageRange[] selectedPages = mPrintPreviewController.getSelectedPages();
932         if (!Arrays.equals(mSelectedPages, selectedPages)) {
933             updateSelectedPages(selectedPages,
934                     getAdjustedPageCount(mPrintedDocument.getDocumentInfo().info));
935         }
936     }
937 
updateSelectedPages(PageRange[] selectedPages, int pageInDocumentCount)938     private void updateSelectedPages(PageRange[] selectedPages, int pageInDocumentCount) {
939         if (selectedPages == null || selectedPages.length <= 0) {
940             return;
941         }
942 
943         selectedPages = PageRangeUtils.normalize(selectedPages);
944 
945         // Handle the case where all pages are specified explicitly
946         // instead of the *all pages* constant.
947         if (PageRangeUtils.isAllPages(selectedPages, pageInDocumentCount)) {
948             selectedPages = new PageRange[] {PageRange.ALL_PAGES};
949         }
950 
951         if (Arrays.equals(mSelectedPages, selectedPages)) {
952             return;
953         }
954 
955         mSelectedPages = selectedPages;
956         mPrintJob.setPages(selectedPages);
957 
958         if (Arrays.equals(selectedPages, PageRange.ALL_PAGES_ARRAY)) {
959             if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
960                 mRangeOptionsSpinner.setSelection(0);
961                 mPageRangeEditText.setText("");
962             }
963         } else if (selectedPages[0].getStart() >= 0
964                 && selectedPages[selectedPages.length - 1].getEnd() < pageInDocumentCount) {
965             if (mRangeOptionsSpinner.getSelectedItemPosition() != 1) {
966                 mRangeOptionsSpinner.setSelection(1);
967             }
968 
969             StringBuilder builder = new StringBuilder();
970             final int pageRangeCount = selectedPages.length;
971             for (int i = 0; i < pageRangeCount; i++) {
972                 if (builder.length() > 0) {
973                     builder.append(',');
974                 }
975 
976                 final int shownStartPage;
977                 final int shownEndPage;
978                 PageRange pageRange = selectedPages[i];
979                 if (pageRange.equals(PageRange.ALL_PAGES)) {
980                     shownStartPage = 1;
981                     shownEndPage = pageInDocumentCount;
982                 } else {
983                     shownStartPage = pageRange.getStart() + 1;
984                     shownEndPage = pageRange.getEnd() + 1;
985                 }
986 
987                 builder.append(shownStartPage);
988 
989                 if (shownStartPage != shownEndPage) {
990                     builder.append('-');
991                     builder.append(shownEndPage);
992                 }
993             }
994 
995             mPageRangeEditText.setText(builder.toString());
996         }
997     }
998 
ensureProgressUiShown()999     private void ensureProgressUiShown() {
1000         if (isFinishing() || isDestroyed()) {
1001             return;
1002         }
1003         if (mUiState != UI_STATE_PROGRESS) {
1004             mUiState = UI_STATE_PROGRESS;
1005             mPrintPreviewController.setUiShown(false);
1006             Fragment fragment = PrintProgressFragment.newInstance();
1007             showFragment(fragment);
1008         }
1009     }
1010 
ensurePreviewUiShown()1011     private void ensurePreviewUiShown() {
1012         if (isFinishing() || isDestroyed()) {
1013             return;
1014         }
1015         if (mUiState != UI_STATE_PREVIEW) {
1016             mUiState = UI_STATE_PREVIEW;
1017             mPrintPreviewController.setUiShown(true);
1018             showFragment(null);
1019         }
1020     }
1021 
ensureErrorUiShown(CharSequence message, int action)1022     private void ensureErrorUiShown(CharSequence message, int action) {
1023         if (isFinishing() || isDestroyed()) {
1024             return;
1025         }
1026         if (mUiState != UI_STATE_ERROR) {
1027             mUiState = UI_STATE_ERROR;
1028             mPrintPreviewController.setUiShown(false);
1029             Fragment fragment = PrintErrorFragment.newInstance(message, action);
1030             showFragment(fragment);
1031         }
1032     }
1033 
showFragment(Fragment newFragment)1034     private void showFragment(Fragment newFragment) {
1035         FragmentTransaction transaction = getFragmentManager().beginTransaction();
1036         Fragment oldFragment = getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
1037         if (oldFragment != null) {
1038             transaction.remove(oldFragment);
1039         }
1040         if (newFragment != null) {
1041             transaction.add(R.id.embedded_content_container, newFragment, FRAGMENT_TAG);
1042         }
1043         transaction.commitAllowingStateLoss();
1044         getFragmentManager().executePendingTransactions();
1045     }
1046 
requestCreatePdfFileOrFinish()1047     private void requestCreatePdfFileOrFinish() {
1048         mPrintedDocument.cancel(false);
1049 
1050         if (mCurrentPrinter == mDestinationSpinnerAdapter.getPdfPrinter()) {
1051             startCreateDocumentActivity();
1052         } else {
1053             transformDocumentAndFinish(null);
1054         }
1055     }
1056 
1057     /**
1058      * Clear the selected page range and update the preview if needed.
1059      */
clearPageRanges()1060     private void clearPageRanges() {
1061         mRangeOptionsSpinner.setSelection(0);
1062         mPageRangeEditText.setError(null);
1063         mPageRangeEditText.setText("");
1064         mSelectedPages = PageRange.ALL_PAGES_ARRAY;
1065 
1066         if (!Arrays.equals(mSelectedPages, mPrintPreviewController.getSelectedPages())) {
1067             updatePrintPreviewController(false);
1068         }
1069     }
1070 
updatePrintAttributesFromCapabilities(PrinterCapabilitiesInfo capabilities)1071     private void updatePrintAttributesFromCapabilities(PrinterCapabilitiesInfo capabilities) {
1072         boolean clearRanges = false;
1073         PrintAttributes defaults = capabilities.getDefaults();
1074 
1075         // Sort the media sizes based on the current locale.
1076         List<MediaSize> sortedMediaSizes = new ArrayList<>(capabilities.getMediaSizes());
1077         Collections.sort(sortedMediaSizes, mMediaSizeComparator);
1078 
1079         PrintAttributes attributes = mPrintJob.getAttributes();
1080 
1081         // Media size.
1082         MediaSize currMediaSize = attributes.getMediaSize();
1083         if (currMediaSize == null) {
1084             clearRanges = true;
1085             attributes.setMediaSize(defaults.getMediaSize());
1086         } else {
1087             MediaSize newMediaSize = null;
1088             boolean isPortrait = currMediaSize.isPortrait();
1089 
1090             // Try to find the current media size in the capabilities as
1091             // it may be in a different orientation.
1092             MediaSize currMediaSizePortrait = currMediaSize.asPortrait();
1093             final int mediaSizeCount = sortedMediaSizes.size();
1094             for (int i = 0; i < mediaSizeCount; i++) {
1095                 MediaSize mediaSize = sortedMediaSizes.get(i);
1096                 if (currMediaSizePortrait.equals(mediaSize.asPortrait())) {
1097                     newMediaSize = mediaSize;
1098                     break;
1099                 }
1100             }
1101             // If we did not find the current media size fall back to default.
1102             if (newMediaSize == null) {
1103                 clearRanges = true;
1104                 newMediaSize = defaults.getMediaSize();
1105             }
1106 
1107             if (newMediaSize != null) {
1108                 if (isPortrait) {
1109                     attributes.setMediaSize(newMediaSize.asPortrait());
1110                 } else {
1111                     attributes.setMediaSize(newMediaSize.asLandscape());
1112                 }
1113             }
1114         }
1115 
1116         // Color mode.
1117         final int colorMode = attributes.getColorMode();
1118         if ((capabilities.getColorModes() & colorMode) == 0) {
1119             attributes.setColorMode(defaults.getColorMode());
1120         }
1121 
1122         // Duplex mode.
1123         final int duplexMode = attributes.getDuplexMode();
1124         if ((capabilities.getDuplexModes() & duplexMode) == 0) {
1125             attributes.setDuplexMode(defaults.getDuplexMode());
1126         }
1127 
1128         // Resolution
1129         Resolution resolution = attributes.getResolution();
1130         if (resolution == null || !capabilities.getResolutions().contains(resolution)) {
1131             attributes.setResolution(defaults.getResolution());
1132         }
1133 
1134         // Margins.
1135         if (!Objects.equals(attributes.getMinMargins(), defaults.getMinMargins())) {
1136             clearRanges = true;
1137         }
1138         attributes.setMinMargins(defaults.getMinMargins());
1139 
1140         if (clearRanges) {
1141             clearPageRanges();
1142         }
1143     }
1144 
updateDocument(boolean clearLastError)1145     private boolean updateDocument(boolean clearLastError) {
1146         if (!clearLastError && mPrintedDocument.hasUpdateError()) {
1147             return false;
1148         }
1149 
1150         if (clearLastError && mPrintedDocument.hasUpdateError()) {
1151             mPrintedDocument.clearUpdateError();
1152         }
1153 
1154         final boolean preview = mState != STATE_PRINT_CONFIRMED;
1155         final PageRange[] pages;
1156         if (preview) {
1157             pages = mPrintPreviewController.getRequestedPages();
1158         } else {
1159             pages = mPrintPreviewController.getSelectedPages();
1160         }
1161 
1162         final boolean willUpdate = mPrintedDocument.update(mPrintJob.getAttributes(),
1163                 pages, preview);
1164 
1165         if (willUpdate && !mPrintedDocument.hasLaidOutPages()) {
1166             // When the update is done we update the print preview.
1167             mProgressMessageController.post();
1168             return true;
1169         } else if (!willUpdate) {
1170             // Update preview.
1171             updatePrintPreviewController(false);
1172         }
1173 
1174         return false;
1175     }
1176 
addCurrentPrinterToHistory()1177     private void addCurrentPrinterToHistory() {
1178         if (mCurrentPrinter != null) {
1179             PrinterId fakePdfPrinterId = mDestinationSpinnerAdapter.getPdfPrinter().getId();
1180             if (!mCurrentPrinter.getId().equals(fakePdfPrinterId)) {
1181                 mPrinterRegistry.addHistoricalPrinter(mCurrentPrinter);
1182             }
1183         }
1184     }
1185 
cancelPrint()1186     private void cancelPrint() {
1187         setState(STATE_PRINT_CANCELED);
1188         updateOptionsUi();
1189         mPrintedDocument.cancel(true);
1190         doFinish();
1191     }
1192 
1193     /**
1194      * Update the selected pages from the text field.
1195      */
updateSelectedPagesFromTextField()1196     private void updateSelectedPagesFromTextField() {
1197         PageRange[] selectedPages = computeSelectedPages();
1198         if (!Arrays.equals(mSelectedPages, selectedPages)) {
1199             mSelectedPages = selectedPages;
1200             // Update preview.
1201             updatePrintPreviewController(false);
1202         }
1203     }
1204 
confirmPrint()1205     private void confirmPrint() {
1206         setState(STATE_PRINT_CONFIRMED);
1207 
1208         MetricsLogger.count(this, "print_confirmed", 1);
1209 
1210         updateOptionsUi();
1211         addCurrentPrinterToHistory();
1212         setUserPrinted();
1213 
1214         // updateSelectedPagesFromTextField migth update the preview, hence apply the preview first
1215         updateSelectedPagesFromPreview();
1216         updateSelectedPagesFromTextField();
1217 
1218         mPrintPreviewController.closeOptions();
1219 
1220         if (canUpdateDocument()) {
1221             updateDocument(false);
1222         }
1223 
1224         if (!mPrintedDocument.isUpdating()) {
1225             requestCreatePdfFileOrFinish();
1226         }
1227     }
1228 
bindUi()1229     private void bindUi() {
1230         // Summary
1231         mSummaryContainer = findViewById(R.id.summary_content);
1232         mSummaryCopies = (TextView) findViewById(R.id.copies_count_summary);
1233         mSummaryPaperSize = (TextView) findViewById(R.id.paper_size_summary);
1234 
1235         // Options container
1236         mOptionsContent = (PrintContentView) findViewById(R.id.options_content);
1237         mOptionsContent.setOptionsStateChangeListener(this);
1238         mOptionsContent.setOpenOptionsController(this);
1239 
1240         OnItemSelectedListener itemSelectedListener = new MyOnItemSelectedListener();
1241         OnClickListener clickListener = new MyClickListener();
1242 
1243         // Copies
1244         mCopiesEditText = (EditText) findViewById(R.id.copies_edittext);
1245         mCopiesEditText.setOnFocusChangeListener(mSelectAllOnFocusListener);
1246         mCopiesEditText.setText(MIN_COPIES_STRING);
1247         mCopiesEditText.setSelection(mCopiesEditText.getText().length());
1248         mCopiesEditText.addTextChangedListener(new EditTextWatcher());
1249 
1250         // Destination.
1251         mPrintersObserver = new PrintersObserver();
1252         mDestinationSpinnerAdapter.registerDataSetObserver(mPrintersObserver);
1253         mDestinationSpinner = (Spinner) findViewById(R.id.destination_spinner);
1254         mDestinationSpinner.setAdapter(mDestinationSpinnerAdapter);
1255         mDestinationSpinner.setOnItemSelectedListener(itemSelectedListener);
1256 
1257         // Media size.
1258         mMediaSizeSpinnerAdapter = new ArrayAdapter<>(
1259                 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1260         mMediaSizeSpinner = (Spinner) findViewById(R.id.paper_size_spinner);
1261         mMediaSizeSpinner.setAdapter(mMediaSizeSpinnerAdapter);
1262         mMediaSizeSpinner.setOnItemSelectedListener(itemSelectedListener);
1263 
1264         // Color mode.
1265         mColorModeSpinnerAdapter = new ArrayAdapter<>(
1266                 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1267         mColorModeSpinner = (Spinner) findViewById(R.id.color_spinner);
1268         mColorModeSpinner.setAdapter(mColorModeSpinnerAdapter);
1269         mColorModeSpinner.setOnItemSelectedListener(itemSelectedListener);
1270 
1271         // Duplex mode.
1272         mDuplexModeSpinnerAdapter = new ArrayAdapter<>(
1273                 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1274         mDuplexModeSpinner = (Spinner) findViewById(R.id.duplex_spinner);
1275         mDuplexModeSpinner.setAdapter(mDuplexModeSpinnerAdapter);
1276         mDuplexModeSpinner.setOnItemSelectedListener(itemSelectedListener);
1277 
1278         // Orientation
1279         mOrientationSpinnerAdapter = new ArrayAdapter<>(
1280                 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1281         String[] orientationLabels = getResources().getStringArray(
1282                 R.array.orientation_labels);
1283         mOrientationSpinnerAdapter.add(new SpinnerItem<>(
1284                 ORIENTATION_PORTRAIT, orientationLabels[0]));
1285         mOrientationSpinnerAdapter.add(new SpinnerItem<>(
1286                 ORIENTATION_LANDSCAPE, orientationLabels[1]));
1287         mOrientationSpinner = (Spinner) findViewById(R.id.orientation_spinner);
1288         mOrientationSpinner.setAdapter(mOrientationSpinnerAdapter);
1289         mOrientationSpinner.setOnItemSelectedListener(itemSelectedListener);
1290 
1291         // Range options
1292         ArrayAdapter<SpinnerItem<Integer>> rangeOptionsSpinnerAdapter = new ArrayAdapter<>(
1293                 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1294         mRangeOptionsSpinner = (Spinner) findViewById(R.id.range_options_spinner);
1295         mRangeOptionsSpinner.setAdapter(rangeOptionsSpinnerAdapter);
1296         mRangeOptionsSpinner.setOnItemSelectedListener(itemSelectedListener);
1297         updatePageRangeOptions(PrintDocumentInfo.PAGE_COUNT_UNKNOWN);
1298 
1299         // Page range
1300         mPageRangeTitle = (TextView) findViewById(R.id.page_range_title);
1301         mPageRangeEditText = (EditText) findViewById(R.id.page_range_edittext);
1302         mPageRangeEditText.setVisibility(View.INVISIBLE);
1303         mPageRangeTitle.setVisibility(View.INVISIBLE);
1304         mPageRangeEditText.setOnFocusChangeListener(mSelectAllOnFocusListener);
1305         mPageRangeEditText.addTextChangedListener(new RangeTextWatcher());
1306 
1307         // Advanced options button.
1308         mMoreOptionsButton = (Button) findViewById(R.id.more_options_button);
1309         mMoreOptionsButton.setOnClickListener(clickListener);
1310 
1311         // Print button
1312         mPrintButton = (ImageView) findViewById(R.id.print_button);
1313         mPrintButton.setOnClickListener(clickListener);
1314 
1315         // The UI is now initialized
1316         mIsOptionsUiBound = true;
1317 
1318         // Special prompt instead of destination spinner for the first time the user printed
1319         if (!hasUserEverPrinted()) {
1320             mShowDestinationPrompt = true;
1321 
1322             mSummaryCopies.setEnabled(false);
1323             mSummaryPaperSize.setEnabled(false);
1324 
1325             mDestinationSpinner.setOnTouchListener(new View.OnTouchListener() {
1326                 @Override
1327                 public boolean onTouch(View v, MotionEvent event) {
1328                     mShowDestinationPrompt = false;
1329                     mSummaryCopies.setEnabled(true);
1330                     mSummaryPaperSize.setEnabled(true);
1331                     updateOptionsUi();
1332 
1333                     mDestinationSpinner.setOnTouchListener(null);
1334                     mDestinationSpinnerAdapter.notifyDataSetChanged();
1335 
1336                     return false;
1337                 }
1338             });
1339         }
1340     }
1341 
1342     @Override
onCreateLoader(int id, Bundle args)1343     public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) {
1344         return new PrintServicesLoader((PrintManager) getSystemService(Context.PRINT_SERVICE), this,
1345                 PrintManager.ENABLED_SERVICES);
1346     }
1347 
1348     @Override
onLoadFinished(Loader<List<PrintServiceInfo>> loader, List<PrintServiceInfo> services)1349     public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
1350             List<PrintServiceInfo> services) {
1351         ComponentName newAdvancedPrintOptionsActivity = null;
1352         if (mCurrentPrinter != null && services != null) {
1353             final int numServices = services.size();
1354             for (int i = 0; i < numServices; i++) {
1355                 PrintServiceInfo service = services.get(i);
1356 
1357                 if (service.getComponentName().equals(mCurrentPrinter.getId().getServiceName())) {
1358                     String advancedOptionsActivityName = service.getAdvancedOptionsActivityName();
1359 
1360                     if (!TextUtils.isEmpty(advancedOptionsActivityName)) {
1361                         newAdvancedPrintOptionsActivity = new ComponentName(
1362                                 service.getComponentName().getPackageName(),
1363                                 advancedOptionsActivityName);
1364 
1365                         break;
1366                     }
1367                 }
1368             }
1369         }
1370 
1371         if (!Objects.equals(newAdvancedPrintOptionsActivity, mAdvancedPrintOptionsActivity)) {
1372             mAdvancedPrintOptionsActivity = newAdvancedPrintOptionsActivity;
1373             updateOptionsUi();
1374         }
1375 
1376         boolean newArePrintServicesEnabled = services != null && !services.isEmpty();
1377         if (mArePrintServicesEnabled != newArePrintServicesEnabled) {
1378             mArePrintServicesEnabled = newArePrintServicesEnabled;
1379 
1380             // Reload mDestinationSpinnerAdapter as mArePrintServicesEnabled changed and the adapter
1381             // reads that in DestinationAdapter#getMoreItemTitle
1382             if (mDestinationSpinnerAdapter != null) {
1383                 mDestinationSpinnerAdapter.notifyDataSetChanged();
1384             }
1385         }
1386     }
1387 
1388     @Override
onLoaderReset(Loader<List<PrintServiceInfo>> loader)1389     public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) {
1390         if (!(isFinishing() || isDestroyed())) {
1391             onLoadFinished(loader, null);
1392         }
1393     }
1394 
1395     /**
1396      * A dialog that asks the user to approve a {@link PrintService}. This dialog is automatically
1397      * dismissed if the same {@link PrintService} gets approved by another
1398      * {@link PrintServiceApprovalDialog}.
1399      */
1400     private static final class PrintServiceApprovalDialog extends DialogFragment
1401             implements OnSharedPreferenceChangeListener {
1402         private static final String PRINTSERVICE_KEY = "PRINTSERVICE";
1403         private ApprovedPrintServices mApprovedServices;
1404 
1405         /**
1406          * Create a new {@link PrintServiceApprovalDialog} that ask the user to approve a
1407          * {@link PrintService}.
1408          *
1409          * @param printService The {@link ComponentName} of the service to approve
1410          * @return A new {@link PrintServiceApprovalDialog} that might approve the service
1411          */
newInstance(ComponentName printService)1412         static PrintServiceApprovalDialog newInstance(ComponentName printService) {
1413             PrintServiceApprovalDialog dialog = new PrintServiceApprovalDialog();
1414 
1415             Bundle args = new Bundle();
1416             args.putParcelable(PRINTSERVICE_KEY, printService);
1417             dialog.setArguments(args);
1418 
1419             return dialog;
1420         }
1421 
1422         @Override
onStop()1423         public void onStop() {
1424             super.onStop();
1425 
1426             mApprovedServices.unregisterChangeListener(this);
1427         }
1428 
1429         @Override
onStart()1430         public void onStart() {
1431             super.onStart();
1432 
1433             ComponentName printService = getArguments().getParcelable(PRINTSERVICE_KEY);
1434             synchronized (ApprovedPrintServices.sLock) {
1435                 if (mApprovedServices.isApprovedService(printService)) {
1436                     dismiss();
1437                 } else {
1438                     mApprovedServices.registerChangeListenerLocked(this);
1439                 }
1440             }
1441         }
1442 
1443         @Override
onCreateDialog(Bundle savedInstanceState)1444         public Dialog onCreateDialog(Bundle savedInstanceState) {
1445             super.onCreateDialog(savedInstanceState);
1446 
1447             mApprovedServices = new ApprovedPrintServices(getActivity());
1448 
1449             PackageManager packageManager = getActivity().getPackageManager();
1450             CharSequence serviceLabel;
1451             try {
1452                 ComponentName printService = getArguments().getParcelable(PRINTSERVICE_KEY);
1453 
1454                 serviceLabel = packageManager.getApplicationInfo(printService.getPackageName(), 0)
1455                         .loadLabel(packageManager);
1456             } catch (NameNotFoundException e) {
1457                 serviceLabel = null;
1458             }
1459 
1460             AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
1461             builder.setTitle(getString(R.string.print_service_security_warning_title,
1462                     serviceLabel))
1463                     .setMessage(getString(R.string.print_service_security_warning_summary,
1464                             serviceLabel))
1465                     .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
1466                         @Override
1467                         public void onClick(DialogInterface dialog, int id) {
1468                             ComponentName printService =
1469                                     getArguments().getParcelable(PRINTSERVICE_KEY);
1470                             // Prevent onSharedPreferenceChanged from getting triggered
1471                             mApprovedServices
1472                                     .unregisterChangeListener(PrintServiceApprovalDialog.this);
1473 
1474                             mApprovedServices.addApprovedService(printService);
1475                             ((PrintActivity) getActivity()).confirmPrint();
1476                         }
1477                     })
1478                     .setNegativeButton(android.R.string.cancel, null);
1479 
1480             return builder.create();
1481         }
1482 
1483         @Override
onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)1484         public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
1485             ComponentName printService = getArguments().getParcelable(PRINTSERVICE_KEY);
1486 
1487             synchronized (ApprovedPrintServices.sLock) {
1488                 if (mApprovedServices.isApprovedService(printService)) {
1489                     dismiss();
1490                 }
1491             }
1492         }
1493     }
1494 
1495     private final class MyClickListener implements OnClickListener {
1496         @Override
onClick(View view)1497         public void onClick(View view) {
1498             if (view == mPrintButton) {
1499                 if (mCurrentPrinter != null) {
1500                     if (mDestinationSpinnerAdapter.getPdfPrinter() == mCurrentPrinter) {
1501                         confirmPrint();
1502                     } else {
1503                         ApprovedPrintServices approvedServices =
1504                                 new ApprovedPrintServices(PrintActivity.this);
1505 
1506                         ComponentName printService = mCurrentPrinter.getId().getServiceName();
1507                         if (approvedServices.isApprovedService(printService)) {
1508                             confirmPrint();
1509                         } else {
1510                             PrintServiceApprovalDialog.newInstance(printService)
1511                                     .show(getFragmentManager(), "approve");
1512                         }
1513                     }
1514                 } else {
1515                     cancelPrint();
1516                 }
1517             } else if (view == mMoreOptionsButton) {
1518                 if (mPageRangeEditText.getError() == null) {
1519                     // The selected pages is only applied once the user leaves the text field. A click
1520                     // on this button, does not count as leaving.
1521                     updateSelectedPagesFromTextField();
1522                 }
1523 
1524                 if (mCurrentPrinter != null) {
1525                     startAdvancedPrintOptionsActivity(mCurrentPrinter);
1526                 }
1527             }
1528         }
1529     }
1530 
canPrint(PrinterInfo printer)1531     private static boolean canPrint(PrinterInfo printer) {
1532         return printer.getCapabilities() != null
1533                 && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
1534     }
1535 
1536     /**
1537      * Disable all options UI elements, beside the {@link #mDestinationSpinner}
1538      */
disableOptionsUi()1539     private void disableOptionsUi() {
1540         mCopiesEditText.setEnabled(false);
1541         mCopiesEditText.setFocusable(false);
1542         mMediaSizeSpinner.setEnabled(false);
1543         mColorModeSpinner.setEnabled(false);
1544         mDuplexModeSpinner.setEnabled(false);
1545         mOrientationSpinner.setEnabled(false);
1546         mRangeOptionsSpinner.setEnabled(false);
1547         mPageRangeEditText.setEnabled(false);
1548         mPrintButton.setVisibility(View.GONE);
1549         mMoreOptionsButton.setEnabled(false);
1550     }
1551 
updateOptionsUi()1552     void updateOptionsUi() {
1553         if (!mIsOptionsUiBound) {
1554             return;
1555         }
1556 
1557         // Always update the summary.
1558         updateSummary();
1559 
1560         if (mState == STATE_PRINT_CONFIRMED
1561                 || mState == STATE_PRINT_COMPLETED
1562                 || mState == STATE_PRINT_CANCELED
1563                 || mState == STATE_UPDATE_FAILED
1564                 || mState == STATE_CREATE_FILE_FAILED
1565                 || mState == STATE_PRINTER_UNAVAILABLE
1566                 || mState == STATE_UPDATE_SLOW) {
1567             if (mState != STATE_PRINTER_UNAVAILABLE) {
1568                 mDestinationSpinner.setEnabled(false);
1569             }
1570             disableOptionsUi();
1571             return;
1572         }
1573 
1574         // If no current printer, or it has no capabilities, or it is not
1575         // available, we disable all print options except the destination.
1576         if (mCurrentPrinter == null || !canPrint(mCurrentPrinter)) {
1577             disableOptionsUi();
1578             return;
1579         }
1580 
1581         PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
1582         PrintAttributes defaultAttributes = capabilities.getDefaults();
1583 
1584         // Destination.
1585         mDestinationSpinner.setEnabled(true);
1586 
1587         // Media size.
1588         mMediaSizeSpinner.setEnabled(true);
1589 
1590         List<MediaSize> mediaSizes = new ArrayList<>(capabilities.getMediaSizes());
1591         // Sort the media sizes based on the current locale.
1592         Collections.sort(mediaSizes, mMediaSizeComparator);
1593 
1594         PrintAttributes attributes = mPrintJob.getAttributes();
1595 
1596         // If the media sizes changed, we update the adapter and the spinner.
1597         boolean mediaSizesChanged = false;
1598         final int mediaSizeCount = mediaSizes.size();
1599         if (mediaSizeCount != mMediaSizeSpinnerAdapter.getCount()) {
1600             mediaSizesChanged = true;
1601         } else {
1602             for (int i = 0; i < mediaSizeCount; i++) {
1603                 if (!mediaSizes.get(i).equals(mMediaSizeSpinnerAdapter.getItem(i).value)) {
1604                     mediaSizesChanged = true;
1605                     break;
1606                 }
1607             }
1608         }
1609         if (mediaSizesChanged) {
1610             // Remember the old media size to try selecting it again.
1611             int oldMediaSizeNewIndex = AdapterView.INVALID_POSITION;
1612             MediaSize oldMediaSize = attributes.getMediaSize();
1613 
1614             // Rebuild the adapter data.
1615             mMediaSizeSpinnerAdapter.clear();
1616             for (int i = 0; i < mediaSizeCount; i++) {
1617                 MediaSize mediaSize = mediaSizes.get(i);
1618                 if (oldMediaSize != null
1619                         && mediaSize.asPortrait().equals(oldMediaSize.asPortrait())) {
1620                     // Update the index of the old selection.
1621                     oldMediaSizeNewIndex = i;
1622                 }
1623                 mMediaSizeSpinnerAdapter.add(new SpinnerItem<>(
1624                         mediaSize, mediaSize.getLabel(getPackageManager())));
1625             }
1626 
1627             if (oldMediaSizeNewIndex != AdapterView.INVALID_POSITION) {
1628                 // Select the old media size - nothing really changed.
1629                 if (mMediaSizeSpinner.getSelectedItemPosition() != oldMediaSizeNewIndex) {
1630                     mMediaSizeSpinner.setSelection(oldMediaSizeNewIndex);
1631                 }
1632             } else {
1633                 // Select the first or the default.
1634                 final int mediaSizeIndex = Math.max(mediaSizes.indexOf(
1635                         defaultAttributes.getMediaSize()), 0);
1636                 if (mMediaSizeSpinner.getSelectedItemPosition() != mediaSizeIndex) {
1637                     mMediaSizeSpinner.setSelection(mediaSizeIndex);
1638                 }
1639                 // Respect the orientation of the old selection.
1640                 if (oldMediaSize != null) {
1641                     if (oldMediaSize.isPortrait()) {
1642                         attributes.setMediaSize(mMediaSizeSpinnerAdapter
1643                                 .getItem(mediaSizeIndex).value.asPortrait());
1644                     } else {
1645                         attributes.setMediaSize(mMediaSizeSpinnerAdapter
1646                                 .getItem(mediaSizeIndex).value.asLandscape());
1647                     }
1648                 }
1649             }
1650         }
1651 
1652         // Color mode.
1653         mColorModeSpinner.setEnabled(true);
1654         final int colorModes = capabilities.getColorModes();
1655 
1656         // If the color modes changed, we update the adapter and the spinner.
1657         boolean colorModesChanged = false;
1658         if (Integer.bitCount(colorModes) != mColorModeSpinnerAdapter.getCount()) {
1659             colorModesChanged = true;
1660         } else {
1661             int remainingColorModes = colorModes;
1662             int adapterIndex = 0;
1663             while (remainingColorModes != 0) {
1664                 final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
1665                 final int colorMode = 1 << colorBitOffset;
1666                 remainingColorModes &= ~colorMode;
1667                 if (colorMode != mColorModeSpinnerAdapter.getItem(adapterIndex).value) {
1668                     colorModesChanged = true;
1669                     break;
1670                 }
1671                 adapterIndex++;
1672             }
1673         }
1674         if (colorModesChanged) {
1675             // Remember the old color mode to try selecting it again.
1676             int oldColorModeNewIndex = AdapterView.INVALID_POSITION;
1677             final int oldColorMode = attributes.getColorMode();
1678 
1679             // Rebuild the adapter data.
1680             mColorModeSpinnerAdapter.clear();
1681             String[] colorModeLabels = getResources().getStringArray(R.array.color_mode_labels);
1682             int remainingColorModes = colorModes;
1683             while (remainingColorModes != 0) {
1684                 final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
1685                 final int colorMode = 1 << colorBitOffset;
1686                 if (colorMode == oldColorMode) {
1687                     // Update the index of the old selection.
1688                     oldColorModeNewIndex = mColorModeSpinnerAdapter.getCount();
1689                 }
1690                 remainingColorModes &= ~colorMode;
1691                 mColorModeSpinnerAdapter.add(new SpinnerItem<>(colorMode,
1692                         colorModeLabels[colorBitOffset]));
1693             }
1694             if (oldColorModeNewIndex != AdapterView.INVALID_POSITION) {
1695                 // Select the old color mode - nothing really changed.
1696                 if (mColorModeSpinner.getSelectedItemPosition() != oldColorModeNewIndex) {
1697                     mColorModeSpinner.setSelection(oldColorModeNewIndex);
1698                 }
1699             } else {
1700                 // Select the default.
1701                 final int selectedColorMode = colorModes & defaultAttributes.getColorMode();
1702                 final int itemCount = mColorModeSpinnerAdapter.getCount();
1703                 for (int i = 0; i < itemCount; i++) {
1704                     SpinnerItem<Integer> item = mColorModeSpinnerAdapter.getItem(i);
1705                     if (selectedColorMode == item.value) {
1706                         if (mColorModeSpinner.getSelectedItemPosition() != i) {
1707                             mColorModeSpinner.setSelection(i);
1708                         }
1709                         attributes.setColorMode(selectedColorMode);
1710                         break;
1711                     }
1712                 }
1713             }
1714         }
1715 
1716         // Duplex mode.
1717         mDuplexModeSpinner.setEnabled(true);
1718         final int duplexModes = capabilities.getDuplexModes();
1719 
1720         // If the duplex modes changed, we update the adapter and the spinner.
1721         // Note that we use bit count +1 to account for the no duplex option.
1722         boolean duplexModesChanged = false;
1723         if (Integer.bitCount(duplexModes) != mDuplexModeSpinnerAdapter.getCount()) {
1724             duplexModesChanged = true;
1725         } else {
1726             int remainingDuplexModes = duplexModes;
1727             int adapterIndex = 0;
1728             while (remainingDuplexModes != 0) {
1729                 final int duplexBitOffset = Integer.numberOfTrailingZeros(remainingDuplexModes);
1730                 final int duplexMode = 1 << duplexBitOffset;
1731                 remainingDuplexModes &= ~duplexMode;
1732                 if (duplexMode != mDuplexModeSpinnerAdapter.getItem(adapterIndex).value) {
1733                     duplexModesChanged = true;
1734                     break;
1735                 }
1736                 adapterIndex++;
1737             }
1738         }
1739         if (duplexModesChanged) {
1740             // Remember the old duplex mode to try selecting it again. Also the fallback
1741             // is no duplexing which is always the first item in the dropdown.
1742             int oldDuplexModeNewIndex = AdapterView.INVALID_POSITION;
1743             final int oldDuplexMode = attributes.getDuplexMode();
1744 
1745             // Rebuild the adapter data.
1746             mDuplexModeSpinnerAdapter.clear();
1747             String[] duplexModeLabels = getResources().getStringArray(R.array.duplex_mode_labels);
1748             int remainingDuplexModes = duplexModes;
1749             while (remainingDuplexModes != 0) {
1750                 final int duplexBitOffset = Integer.numberOfTrailingZeros(remainingDuplexModes);
1751                 final int duplexMode = 1 << duplexBitOffset;
1752                 if (duplexMode == oldDuplexMode) {
1753                     // Update the index of the old selection.
1754                     oldDuplexModeNewIndex = mDuplexModeSpinnerAdapter.getCount();
1755                 }
1756                 remainingDuplexModes &= ~duplexMode;
1757                 mDuplexModeSpinnerAdapter.add(new SpinnerItem<>(duplexMode,
1758                         duplexModeLabels[duplexBitOffset]));
1759             }
1760 
1761             if (oldDuplexModeNewIndex != AdapterView.INVALID_POSITION) {
1762                 // Select the old duplex mode - nothing really changed.
1763                 if (mDuplexModeSpinner.getSelectedItemPosition() != oldDuplexModeNewIndex) {
1764                     mDuplexModeSpinner.setSelection(oldDuplexModeNewIndex);
1765                 }
1766             } else {
1767                 // Select the default.
1768                 final int selectedDuplexMode = defaultAttributes.getDuplexMode();
1769                 final int itemCount = mDuplexModeSpinnerAdapter.getCount();
1770                 for (int i = 0; i < itemCount; i++) {
1771                     SpinnerItem<Integer> item = mDuplexModeSpinnerAdapter.getItem(i);
1772                     if (selectedDuplexMode == item.value) {
1773                         if (mDuplexModeSpinner.getSelectedItemPosition() != i) {
1774                             mDuplexModeSpinner.setSelection(i);
1775                         }
1776                         attributes.setDuplexMode(selectedDuplexMode);
1777                         break;
1778                     }
1779                 }
1780             }
1781         }
1782 
1783         mDuplexModeSpinner.setEnabled(mDuplexModeSpinnerAdapter.getCount() > 1);
1784 
1785         // Orientation
1786         mOrientationSpinner.setEnabled(true);
1787         MediaSize mediaSize = attributes.getMediaSize();
1788         if (mediaSize != null) {
1789             if (mediaSize.isPortrait()
1790                     && mOrientationSpinner.getSelectedItemPosition() != 0) {
1791                 mOrientationSpinner.setSelection(0);
1792             } else if (!mediaSize.isPortrait()
1793                     && mOrientationSpinner.getSelectedItemPosition() != 1) {
1794                 mOrientationSpinner.setSelection(1);
1795             }
1796         }
1797 
1798         // Range options
1799         PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
1800         final int pageCount = getAdjustedPageCount(info);
1801         if (pageCount > 0) {
1802             if (info != null) {
1803                 if (pageCount == 1) {
1804                     mRangeOptionsSpinner.setEnabled(false);
1805                 } else {
1806                     mRangeOptionsSpinner.setEnabled(true);
1807                     if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
1808                         if (!mPageRangeEditText.isEnabled()) {
1809                             mPageRangeEditText.setEnabled(true);
1810                             mPageRangeEditText.setVisibility(View.VISIBLE);
1811                             mPageRangeTitle.setVisibility(View.VISIBLE);
1812                             mPageRangeEditText.requestFocus();
1813                             InputMethodManager imm = (InputMethodManager)
1814                                     getSystemService(Context.INPUT_METHOD_SERVICE);
1815                             imm.showSoftInput(mPageRangeEditText, 0);
1816                         }
1817                     } else {
1818                         mPageRangeEditText.setEnabled(false);
1819                         mPageRangeEditText.setVisibility(View.INVISIBLE);
1820                         mPageRangeTitle.setVisibility(View.INVISIBLE);
1821                     }
1822                 }
1823             } else {
1824                 if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
1825                     mRangeOptionsSpinner.setSelection(0);
1826                     mPageRangeEditText.setText("");
1827                 }
1828                 mRangeOptionsSpinner.setEnabled(false);
1829                 mPageRangeEditText.setEnabled(false);
1830                 mPageRangeEditText.setVisibility(View.INVISIBLE);
1831                 mPageRangeTitle.setVisibility(View.INVISIBLE);
1832             }
1833         }
1834 
1835         final int newPageCount = getAdjustedPageCount(info);
1836         if (newPageCount != mCurrentPageCount) {
1837             mCurrentPageCount = newPageCount;
1838             updatePageRangeOptions(newPageCount);
1839         }
1840 
1841         // Advanced print options
1842         if (mAdvancedPrintOptionsActivity != null) {
1843             mMoreOptionsButton.setVisibility(View.VISIBLE);
1844             mMoreOptionsButton.setEnabled(true);
1845         } else {
1846             mMoreOptionsButton.setVisibility(View.GONE);
1847             mMoreOptionsButton.setEnabled(false);
1848         }
1849 
1850         // Print
1851         if (mDestinationSpinnerAdapter.getPdfPrinter() != mCurrentPrinter) {
1852             mPrintButton.setImageResource(com.android.internal.R.drawable.ic_print);
1853             mPrintButton.setContentDescription(getString(R.string.print_button));
1854         } else {
1855             mPrintButton.setImageResource(R.drawable.ic_menu_savetopdf);
1856             mPrintButton.setContentDescription(getString(R.string.savetopdf_button));
1857         }
1858         if (!mPrintedDocument.getDocumentInfo().laidout
1859                 ||(mRangeOptionsSpinner.getSelectedItemPosition() == 1
1860                 && (TextUtils.isEmpty(mPageRangeEditText.getText()) || hasErrors()))
1861                 || (mRangeOptionsSpinner.getSelectedItemPosition() == 0
1862                 && (mPrintedDocument.getDocumentInfo() == null || hasErrors()))) {
1863             mPrintButton.setVisibility(View.GONE);
1864         } else {
1865             mPrintButton.setVisibility(View.VISIBLE);
1866         }
1867 
1868         // Copies
1869         if (mDestinationSpinnerAdapter.getPdfPrinter() != mCurrentPrinter) {
1870             mCopiesEditText.setEnabled(true);
1871             mCopiesEditText.setFocusableInTouchMode(true);
1872         } else {
1873             CharSequence text = mCopiesEditText.getText();
1874             if (TextUtils.isEmpty(text) || !MIN_COPIES_STRING.equals(text.toString())) {
1875                 mCopiesEditText.setText(MIN_COPIES_STRING);
1876             }
1877             mCopiesEditText.setEnabled(false);
1878             mCopiesEditText.setFocusable(false);
1879         }
1880         if (mCopiesEditText.getError() == null
1881                 && TextUtils.isEmpty(mCopiesEditText.getText())) {
1882             mCopiesEditText.setText(MIN_COPIES_STRING);
1883             mCopiesEditText.requestFocus();
1884         }
1885 
1886         if (mShowDestinationPrompt) {
1887             disableOptionsUi();
1888         }
1889     }
1890 
updateSummary()1891     private void updateSummary() {
1892         if (!mIsOptionsUiBound) {
1893             return;
1894         }
1895 
1896         CharSequence copiesText = null;
1897         CharSequence mediaSizeText = null;
1898 
1899         if (!TextUtils.isEmpty(mCopiesEditText.getText())) {
1900             copiesText = mCopiesEditText.getText();
1901             mSummaryCopies.setText(copiesText);
1902         }
1903 
1904         final int selectedMediaIndex = mMediaSizeSpinner.getSelectedItemPosition();
1905         if (selectedMediaIndex >= 0) {
1906             SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(selectedMediaIndex);
1907             mediaSizeText = mediaItem.label;
1908             mSummaryPaperSize.setText(mediaSizeText);
1909         }
1910 
1911         if (!TextUtils.isEmpty(copiesText) && !TextUtils.isEmpty(mediaSizeText)) {
1912             String summaryText = getString(R.string.summary_template, copiesText, mediaSizeText);
1913             mSummaryContainer.setContentDescription(summaryText);
1914         }
1915     }
1916 
updatePageRangeOptions(int pageCount)1917     private void updatePageRangeOptions(int pageCount) {
1918         @SuppressWarnings("unchecked")
1919         ArrayAdapter<SpinnerItem<Integer>> rangeOptionsSpinnerAdapter =
1920                 (ArrayAdapter<SpinnerItem<Integer>>) mRangeOptionsSpinner.getAdapter();
1921         rangeOptionsSpinnerAdapter.clear();
1922 
1923         final int[] rangeOptionsValues = getResources().getIntArray(
1924                 R.array.page_options_values);
1925 
1926         String pageCountLabel = (pageCount > 0) ? String.valueOf(pageCount) : "";
1927         String[] rangeOptionsLabels = new String[] {
1928             getString(R.string.template_all_pages, pageCountLabel),
1929             getString(R.string.template_page_range, pageCountLabel)
1930         };
1931 
1932         final int rangeOptionsCount = rangeOptionsLabels.length;
1933         for (int i = 0; i < rangeOptionsCount; i++) {
1934             rangeOptionsSpinnerAdapter.add(new SpinnerItem<>(
1935                     rangeOptionsValues[i], rangeOptionsLabels[i]));
1936         }
1937     }
1938 
computeSelectedPages()1939     private PageRange[] computeSelectedPages() {
1940         if (hasErrors()) {
1941             return null;
1942         }
1943 
1944         if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
1945             PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
1946             final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
1947 
1948             return PageRangeUtils.parsePageRanges(mPageRangeEditText.getText(), pageCount);
1949         }
1950 
1951         return PageRange.ALL_PAGES_ARRAY;
1952     }
1953 
getAdjustedPageCount(PrintDocumentInfo info)1954     private int getAdjustedPageCount(PrintDocumentInfo info) {
1955         if (info != null) {
1956             final int pageCount = info.getPageCount();
1957             if (pageCount != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) {
1958                 return pageCount;
1959             }
1960         }
1961         // If the app does not tell us how many pages are in the
1962         // doc we ask for all pages and use the document page count.
1963         return mPrintPreviewController.getFilePageCount();
1964     }
1965 
hasErrors()1966     private boolean hasErrors() {
1967         return (mCopiesEditText.getError() != null)
1968                 || (mPageRangeEditText.getVisibility() == View.VISIBLE
1969                 && mPageRangeEditText.getError() != null);
1970     }
1971 
onPrinterAvailable(PrinterInfo printer)1972     public void onPrinterAvailable(PrinterInfo printer) {
1973         if (mCurrentPrinter != null && mCurrentPrinter.equals(printer)) {
1974             setState(STATE_CONFIGURING);
1975             if (canUpdateDocument()) {
1976                 updateDocument(false);
1977             }
1978             ensurePreviewUiShown();
1979             updateOptionsUi();
1980         }
1981     }
1982 
onPrinterUnavailable(PrinterInfo printer)1983     public void onPrinterUnavailable(PrinterInfo printer) {
1984         if (mCurrentPrinter.getId().equals(printer.getId())) {
1985             setState(STATE_PRINTER_UNAVAILABLE);
1986             mPrintedDocument.cancel(false);
1987             ensureErrorUiShown(getString(R.string.print_error_printer_unavailable),
1988                     PrintErrorFragment.ACTION_NONE);
1989             updateOptionsUi();
1990         }
1991     }
1992 
canUpdateDocument()1993     private boolean canUpdateDocument() {
1994         if (mPrintedDocument.isDestroyed()) {
1995             return false;
1996         }
1997 
1998         if (hasErrors()) {
1999             return false;
2000         }
2001 
2002         PrintAttributes attributes = mPrintJob.getAttributes();
2003 
2004         final int colorMode = attributes.getColorMode();
2005         if (colorMode != PrintAttributes.COLOR_MODE_COLOR
2006                 && colorMode != PrintAttributes.COLOR_MODE_MONOCHROME) {
2007             return false;
2008         }
2009         if (attributes.getMediaSize() == null) {
2010             return false;
2011         }
2012         if (attributes.getMinMargins() == null) {
2013             return false;
2014         }
2015         if (attributes.getResolution() == null) {
2016             return false;
2017         }
2018 
2019         if (mCurrentPrinter == null) {
2020             return false;
2021         }
2022         PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
2023         if (capabilities == null) {
2024             return false;
2025         }
2026         if (mCurrentPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE) {
2027             return false;
2028         }
2029 
2030         return true;
2031     }
2032 
transformDocumentAndFinish(final Uri writeToUri)2033     private void transformDocumentAndFinish(final Uri writeToUri) {
2034         // If saving to PDF, apply the attibutes as we are acting as a print service.
2035         PrintAttributes attributes = mDestinationSpinnerAdapter.getPdfPrinter() == mCurrentPrinter
2036                 ?  mPrintJob.getAttributes() : null;
2037         new DocumentTransformer(this, mPrintJob, mFileProvider, attributes, new Runnable() {
2038             @Override
2039             public void run() {
2040                 if (writeToUri != null) {
2041                     mPrintedDocument.writeContent(getContentResolver(), writeToUri);
2042                 }
2043                 setState(STATE_PRINT_COMPLETED);
2044                 doFinish();
2045             }
2046         }).transform();
2047     }
2048 
doFinish()2049     private void doFinish() {
2050         if (mPrintedDocument != null && mPrintedDocument.isUpdating()) {
2051             // The printedDocument will call doFinish() when the current command finishes
2052             return;
2053         }
2054 
2055         if (mIsFinishing) {
2056             return;
2057         }
2058 
2059         mIsFinishing = true;
2060 
2061         if (mPrinterRegistry != null) {
2062             mPrinterRegistry.setTrackedPrinter(null);
2063         }
2064 
2065         if (mPrintersObserver != null) {
2066             mDestinationSpinnerAdapter.unregisterDataSetObserver(mPrintersObserver);
2067         }
2068 
2069         if (mSpoolerProvider != null) {
2070             mSpoolerProvider.destroy();
2071         }
2072 
2073         if (mProgressMessageController != null) {
2074             setState(mProgressMessageController.cancel());
2075         }
2076 
2077         if (mState != STATE_INITIALIZING) {
2078             mPrintedDocument.finish();
2079             mPrintedDocument.destroy();
2080             mPrintPreviewController.destroy(new Runnable() {
2081                 @Override
2082                 public void run() {
2083                     finish();
2084                 }
2085             });
2086         } else {
2087             finish();
2088         }
2089     }
2090 
2091     private final class SpinnerItem<T> {
2092         final T value;
2093         final CharSequence label;
2094 
SpinnerItem(T value, CharSequence label)2095         public SpinnerItem(T value, CharSequence label) {
2096             this.value = value;
2097             this.label = label;
2098         }
2099 
2100         @Override
toString()2101         public String toString() {
2102             return label.toString();
2103         }
2104     }
2105 
2106     private final class PrinterAvailabilityDetector implements Runnable {
2107         private static final long UNAVAILABLE_TIMEOUT_MILLIS = 10000; // 10sec
2108 
2109         private boolean mPosted;
2110 
2111         private boolean mPrinterUnavailable;
2112 
2113         private PrinterInfo mPrinter;
2114 
updatePrinter(PrinterInfo printer)2115         public void updatePrinter(PrinterInfo printer) {
2116             if (printer.equals(mDestinationSpinnerAdapter.getPdfPrinter())) {
2117                 return;
2118             }
2119 
2120             final boolean available = printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE
2121                     && printer.getCapabilities() != null;
2122             final boolean notifyIfAvailable;
2123 
2124             if (mPrinter == null || !mPrinter.getId().equals(printer.getId())) {
2125                 notifyIfAvailable = true;
2126                 unpostIfNeeded();
2127                 mPrinterUnavailable = false;
2128                 mPrinter = new PrinterInfo.Builder(printer).build();
2129             } else {
2130                 notifyIfAvailable =
2131                         (mPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE
2132                                 && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE)
2133                                 || (mPrinter.getCapabilities() == null
2134                                 && printer.getCapabilities() != null);
2135                 mPrinter = printer;
2136             }
2137 
2138             if (available) {
2139                 unpostIfNeeded();
2140                 mPrinterUnavailable = false;
2141                 if (notifyIfAvailable) {
2142                     onPrinterAvailable(mPrinter);
2143                 }
2144             } else {
2145                 if (!mPrinterUnavailable) {
2146                     postIfNeeded();
2147                 }
2148             }
2149         }
2150 
cancel()2151         public void cancel() {
2152             unpostIfNeeded();
2153             mPrinterUnavailable = false;
2154         }
2155 
postIfNeeded()2156         private void postIfNeeded() {
2157             if (!mPosted) {
2158                 mPosted = true;
2159                 mDestinationSpinner.postDelayed(this, UNAVAILABLE_TIMEOUT_MILLIS);
2160             }
2161         }
2162 
unpostIfNeeded()2163         private void unpostIfNeeded() {
2164             if (mPosted) {
2165                 mPosted = false;
2166                 mDestinationSpinner.removeCallbacks(this);
2167             }
2168         }
2169 
2170         @Override
run()2171         public void run() {
2172             mPosted = false;
2173             mPrinterUnavailable = true;
2174             onPrinterUnavailable(mPrinter);
2175         }
2176     }
2177 
2178     private static final class PrinterHolder {
2179         PrinterInfo printer;
2180         boolean removed;
2181 
PrinterHolder(PrinterInfo printer)2182         public PrinterHolder(PrinterInfo printer) {
2183             this.printer = printer;
2184         }
2185     }
2186 
2187 
2188     /**
2189      * Check if the user has ever printed a document
2190      *
2191      * @return true iff the user has ever printed a document
2192      */
hasUserEverPrinted()2193     private boolean hasUserEverPrinted() {
2194         SharedPreferences preferences = getSharedPreferences(HAS_PRINTED_PREF, MODE_PRIVATE);
2195 
2196         return preferences.getBoolean(HAS_PRINTED_PREF, false);
2197     }
2198 
2199     /**
2200      * Remember that the user printed a document
2201      */
setUserPrinted()2202     private void setUserPrinted() {
2203         SharedPreferences preferences = getSharedPreferences(HAS_PRINTED_PREF, MODE_PRIVATE);
2204 
2205         if (!preferences.getBoolean(HAS_PRINTED_PREF, false)) {
2206             SharedPreferences.Editor edit = preferences.edit();
2207 
2208             edit.putBoolean(HAS_PRINTED_PREF, true);
2209             edit.apply();
2210         }
2211     }
2212 
2213     private final class DestinationAdapter extends BaseAdapter
2214             implements PrinterRegistry.OnPrintersChangeListener {
2215         private final List<PrinterHolder> mPrinterHolders = new ArrayList<>();
2216 
2217         private final PrinterHolder mFakePdfPrinterHolder;
2218 
2219         private boolean mHistoricalPrintersLoaded;
2220 
2221         /**
2222          * Has the {@link #mDestinationSpinner} ever used a view from printer_dropdown_prompt
2223          */
2224         private boolean hadPromptView;
2225 
DestinationAdapter()2226         public DestinationAdapter() {
2227             mHistoricalPrintersLoaded = mPrinterRegistry.areHistoricalPrintersLoaded();
2228             if (mHistoricalPrintersLoaded) {
2229                 addPrinters(mPrinterHolders, mPrinterRegistry.getPrinters());
2230             }
2231             mPrinterRegistry.setOnPrintersChangeListener(this);
2232             mFakePdfPrinterHolder = new PrinterHolder(createFakePdfPrinter());
2233         }
2234 
getPdfPrinter()2235         public PrinterInfo getPdfPrinter() {
2236             return mFakePdfPrinterHolder.printer;
2237         }
2238 
getPrinterIndex(PrinterId printerId)2239         public int getPrinterIndex(PrinterId printerId) {
2240             for (int i = 0; i < getCount(); i++) {
2241                 PrinterHolder printerHolder = (PrinterHolder) getItem(i);
2242                 if (printerHolder != null && !printerHolder.removed
2243                         && printerHolder.printer.getId().equals(printerId)) {
2244                     return i;
2245                 }
2246             }
2247             return AdapterView.INVALID_POSITION;
2248         }
2249 
ensurePrinterInVisibleAdapterPosition(PrinterInfo printer)2250         public void ensurePrinterInVisibleAdapterPosition(PrinterInfo printer) {
2251             final int printerCount = mPrinterHolders.size();
2252             boolean isKnownPrinter = false;
2253             for (int i = 0; i < printerCount; i++) {
2254                 PrinterHolder printerHolder = mPrinterHolders.get(i);
2255 
2256                 if (printerHolder.printer.getId().equals(printer.getId())) {
2257                     isKnownPrinter = true;
2258 
2259                     // If already in the list - do nothing.
2260                     if (i < getCount() - 2) {
2261                         break;
2262                     }
2263                     // Else replace the last one (two items are not printers).
2264                     final int lastPrinterIndex = getCount() - 3;
2265                     mPrinterHolders.set(i, mPrinterHolders.get(lastPrinterIndex));
2266                     mPrinterHolders.set(lastPrinterIndex, printerHolder);
2267                     break;
2268                 }
2269             }
2270 
2271             if (!isKnownPrinter) {
2272                 PrinterHolder printerHolder = new PrinterHolder(printer);
2273                 printerHolder.removed = true;
2274 
2275                 mPrinterHolders.add(Math.max(0, getCount() - 3), printerHolder);
2276             }
2277 
2278             // Force reload to adjust selection in PrintersObserver.onChanged()
2279             notifyDataSetChanged();
2280         }
2281 
2282         @Override
getCount()2283         public int getCount() {
2284             if (mHistoricalPrintersLoaded) {
2285                 return Math.min(mPrinterHolders.size() + 2, DEST_ADAPTER_MAX_ITEM_COUNT);
2286             }
2287             return 0;
2288         }
2289 
2290         @Override
isEnabled(int position)2291         public boolean isEnabled(int position) {
2292             Object item = getItem(position);
2293             if (item instanceof PrinterHolder) {
2294                 PrinterHolder printerHolder = (PrinterHolder) item;
2295                 return !printerHolder.removed
2296                         && printerHolder.printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
2297             }
2298             return true;
2299         }
2300 
2301         @Override
getItem(int position)2302         public Object getItem(int position) {
2303             if (mPrinterHolders.isEmpty()) {
2304                 if (position == 0) {
2305                     return mFakePdfPrinterHolder;
2306                 }
2307             } else {
2308                 if (position < 1) {
2309                     return mPrinterHolders.get(position);
2310                 }
2311                 if (position == 1) {
2312                     return mFakePdfPrinterHolder;
2313                 }
2314                 if (position < getCount() - 1) {
2315                     return mPrinterHolders.get(position - 1);
2316                 }
2317             }
2318             return null;
2319         }
2320 
2321         @Override
getItemId(int position)2322         public long getItemId(int position) {
2323             if (mPrinterHolders.isEmpty()) {
2324                 if (position == 0) {
2325                     return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
2326                 } else if (position == 1) {
2327                     return DEST_ADAPTER_ITEM_ID_MORE;
2328                 }
2329             } else {
2330                 if (position == 1) {
2331                     return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
2332                 }
2333                 if (position == getCount() - 1) {
2334                     return DEST_ADAPTER_ITEM_ID_MORE;
2335                 }
2336             }
2337             return position;
2338         }
2339 
2340         @Override
getDropDownView(int position, View convertView, ViewGroup parent)2341         public View getDropDownView(int position, View convertView, ViewGroup parent) {
2342             View view = getView(position, convertView, parent);
2343             view.setEnabled(isEnabled(position));
2344             return view;
2345         }
2346 
getMoreItemTitle()2347         private String getMoreItemTitle() {
2348             if (mArePrintServicesEnabled) {
2349                 return getString(R.string.all_printers);
2350             } else {
2351                 return getString(R.string.print_add_printer);
2352             }
2353         }
2354 
2355         @Override
getView(int position, View convertView, ViewGroup parent)2356         public View getView(int position, View convertView, ViewGroup parent) {
2357             if (mShowDestinationPrompt) {
2358                 if (convertView == null) {
2359                     convertView = getLayoutInflater().inflate(
2360                             R.layout.printer_dropdown_prompt, parent, false);
2361                     hadPromptView = true;
2362                 }
2363 
2364                 return convertView;
2365             } else {
2366                 // We don't know if we got an recyled printer_dropdown_prompt, hence do not use it
2367                 if (hadPromptView || convertView == null) {
2368                     convertView = getLayoutInflater().inflate(
2369                             R.layout.printer_dropdown_item, parent, false);
2370                 }
2371             }
2372 
2373             CharSequence title = null;
2374             CharSequence subtitle = null;
2375             Drawable icon = null;
2376 
2377             if (mPrinterHolders.isEmpty()) {
2378                 if (position == 0 && getPdfPrinter() != null) {
2379                     PrinterHolder printerHolder = (PrinterHolder) getItem(position);
2380                     title = printerHolder.printer.getName();
2381                     icon = getResources().getDrawable(R.drawable.ic_menu_savetopdf, null);
2382                 } else if (position == 1) {
2383                     title = getMoreItemTitle();
2384                 }
2385             } else {
2386                 if (position == 1 && getPdfPrinter() != null) {
2387                     PrinterHolder printerHolder = (PrinterHolder) getItem(position);
2388                     title = printerHolder.printer.getName();
2389                     icon = getResources().getDrawable(R.drawable.ic_menu_savetopdf, null);
2390                 } else if (position == getCount() - 1) {
2391                     title = getMoreItemTitle();
2392                 } else {
2393                     PrinterHolder printerHolder = (PrinterHolder) getItem(position);
2394                     PrinterInfo printInfo = printerHolder.printer;
2395 
2396                     title = printInfo.getName();
2397                     icon = printInfo.loadIcon(PrintActivity.this);
2398                     subtitle = printInfo.getDescription();
2399                 }
2400             }
2401 
2402             TextView titleView = (TextView) convertView.findViewById(R.id.title);
2403             titleView.setText(title);
2404 
2405             TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle);
2406             if (!TextUtils.isEmpty(subtitle)) {
2407                 subtitleView.setText(subtitle);
2408                 subtitleView.setVisibility(View.VISIBLE);
2409             } else {
2410                 subtitleView.setText(null);
2411                 subtitleView.setVisibility(View.GONE);
2412             }
2413 
2414             ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
2415             if (icon != null) {
2416                 iconView.setVisibility(View.VISIBLE);
2417                 if (!isEnabled(position)) {
2418                     icon.mutate();
2419 
2420                     TypedValue value = new TypedValue();
2421                     getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true);
2422                     icon.setAlpha((int)(value.getFloat() * 255));
2423                 }
2424                 iconView.setImageDrawable(icon);
2425             } else {
2426                 iconView.setVisibility(View.INVISIBLE);
2427             }
2428 
2429             return convertView;
2430         }
2431 
2432         @Override
onPrintersChanged(List<PrinterInfo> printers)2433         public void onPrintersChanged(List<PrinterInfo> printers) {
2434             // We rearrange the printers if the user selects a printer
2435             // not shown in the initial short list. Therefore, we have
2436             // to keep the printer order.
2437 
2438             // Check if historical printers are loaded as this adapter is open
2439             // for busyness only if they are. This member is updated here and
2440             // when the adapter is created because the historical printers may
2441             // be loaded before or after the adapter is created.
2442             mHistoricalPrintersLoaded = mPrinterRegistry.areHistoricalPrintersLoaded();
2443 
2444             // No old printers - do not bother keeping their position.
2445             if (mPrinterHolders.isEmpty()) {
2446                 addPrinters(mPrinterHolders, printers);
2447                 notifyDataSetChanged();
2448                 return;
2449             }
2450 
2451             // Add the new printers to a map.
2452             ArrayMap<PrinterId, PrinterInfo> newPrintersMap = new ArrayMap<>();
2453             final int printerCount = printers.size();
2454             for (int i = 0; i < printerCount; i++) {
2455                 PrinterInfo printer = printers.get(i);
2456                 newPrintersMap.put(printer.getId(), printer);
2457             }
2458 
2459             List<PrinterHolder> newPrinterHolders = new ArrayList<>();
2460 
2461             // Update printers we already have which are either updated or removed.
2462             // We do not remove the currently selected printer.
2463             final int oldPrinterCount = mPrinterHolders.size();
2464             for (int i = 0; i < oldPrinterCount; i++) {
2465                 PrinterHolder printerHolder = mPrinterHolders.get(i);
2466                 PrinterId oldPrinterId = printerHolder.printer.getId();
2467                 PrinterInfo updatedPrinter = newPrintersMap.remove(oldPrinterId);
2468 
2469                 if (updatedPrinter != null) {
2470                     printerHolder.printer = updatedPrinter;
2471                     printerHolder.removed = false;
2472                     onPrinterAvailable(printerHolder.printer);
2473                     newPrinterHolders.add(printerHolder);
2474                 } else if (mCurrentPrinter != null && mCurrentPrinter.getId().equals(oldPrinterId)){
2475                     printerHolder.removed = true;
2476                     onPrinterUnavailable(printerHolder.printer);
2477                     newPrinterHolders.add(printerHolder);
2478                 }
2479             }
2480 
2481             // Add the rest of the new printers, i.e. what is left.
2482             addPrinters(newPrinterHolders, newPrintersMap.values());
2483 
2484             mPrinterHolders.clear();
2485             mPrinterHolders.addAll(newPrinterHolders);
2486 
2487             notifyDataSetChanged();
2488         }
2489 
2490         @Override
onPrintersInvalid()2491         public void onPrintersInvalid() {
2492             mPrinterHolders.clear();
2493             notifyDataSetInvalidated();
2494         }
2495 
getPrinterHolder(PrinterId printerId)2496         public PrinterHolder getPrinterHolder(PrinterId printerId) {
2497             final int itemCount = getCount();
2498             for (int i = 0; i < itemCount; i++) {
2499                 Object item = getItem(i);
2500                 if (item instanceof PrinterHolder) {
2501                     PrinterHolder printerHolder = (PrinterHolder) item;
2502                     if (printerId.equals(printerHolder.printer.getId())) {
2503                         return printerHolder;
2504                     }
2505                 }
2506             }
2507             return null;
2508         }
2509 
2510         /**
2511          * Remove a printer from the holders if it is marked as removed.
2512          *
2513          * @param printerId the id of the printer to remove.
2514          *
2515          * @return true iff the printer was removed.
2516          */
pruneRemovedPrinter(PrinterId printerId)2517         public boolean pruneRemovedPrinter(PrinterId printerId) {
2518             final int holderCounts = mPrinterHolders.size();
2519             for (int i = holderCounts - 1; i >= 0; i--) {
2520                 PrinterHolder printerHolder = mPrinterHolders.get(i);
2521 
2522                 if (printerHolder.printer.getId().equals(printerId) && printerHolder.removed) {
2523                     mPrinterHolders.remove(i);
2524                     return true;
2525                 }
2526             }
2527 
2528             return false;
2529         }
2530 
addPrinters(List<PrinterHolder> list, Collection<PrinterInfo> printers)2531         private void addPrinters(List<PrinterHolder> list, Collection<PrinterInfo> printers) {
2532             for (PrinterInfo printer : printers) {
2533                 PrinterHolder printerHolder = new PrinterHolder(printer);
2534                 list.add(printerHolder);
2535             }
2536         }
2537 
createFakePdfPrinter()2538         private PrinterInfo createFakePdfPrinter() {
2539             ArraySet<MediaSize> allMediaSizes = MediaSize.getAllPredefinedSizes();
2540             MediaSize defaultMediaSize = MediaSizeUtils.getDefault(PrintActivity.this);
2541 
2542             PrinterId printerId = new PrinterId(getComponentName(), "PDF printer");
2543 
2544             PrinterCapabilitiesInfo.Builder builder =
2545                     new PrinterCapabilitiesInfo.Builder(printerId);
2546 
2547             final int mediaSizeCount = allMediaSizes.size();
2548             for (int i = 0; i < mediaSizeCount; i++) {
2549                 MediaSize mediaSize = allMediaSizes.valueAt(i);
2550                 builder.addMediaSize(mediaSize, mediaSize.equals(defaultMediaSize));
2551             }
2552 
2553             builder.addResolution(new Resolution("PDF resolution", "PDF resolution", 300, 300),
2554                     true);
2555             builder.setColorModes(PrintAttributes.COLOR_MODE_COLOR
2556                     | PrintAttributes.COLOR_MODE_MONOCHROME, PrintAttributes.COLOR_MODE_COLOR);
2557 
2558             return new PrinterInfo.Builder(printerId, getString(R.string.save_as_pdf),
2559                     PrinterInfo.STATUS_IDLE).setCapabilities(builder.build()).build();
2560         }
2561     }
2562 
2563     private final class PrintersObserver extends DataSetObserver {
2564         @Override
onChanged()2565         public void onChanged() {
2566             PrinterInfo oldPrinterState = mCurrentPrinter;
2567             if (oldPrinterState == null) {
2568                 return;
2569             }
2570 
2571             PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
2572                     oldPrinterState.getId());
2573             PrinterInfo newPrinterState = printerHolder.printer;
2574 
2575             if (printerHolder.removed) {
2576                 onPrinterUnavailable(newPrinterState);
2577             }
2578 
2579             if (mDestinationSpinner.getSelectedItem() != printerHolder) {
2580                 mDestinationSpinner.setSelection(
2581                         mDestinationSpinnerAdapter.getPrinterIndex(newPrinterState.getId()));
2582             }
2583 
2584             if (oldPrinterState.equals(newPrinterState)) {
2585                 return;
2586             }
2587 
2588             PrinterCapabilitiesInfo oldCapab = oldPrinterState.getCapabilities();
2589             PrinterCapabilitiesInfo newCapab = newPrinterState.getCapabilities();
2590 
2591             final boolean hadCabab = oldCapab != null;
2592             final boolean hasCapab = newCapab != null;
2593             final boolean gotCapab = oldCapab == null && newCapab != null;
2594             final boolean lostCapab = oldCapab != null && newCapab == null;
2595             final boolean capabChanged = capabilitiesChanged(oldCapab, newCapab);
2596 
2597             final int oldStatus = oldPrinterState.getStatus();
2598             final int newStatus = newPrinterState.getStatus();
2599 
2600             final boolean isActive = newStatus != PrinterInfo.STATUS_UNAVAILABLE;
2601             final boolean becameActive = (oldStatus == PrinterInfo.STATUS_UNAVAILABLE
2602                     && oldStatus != newStatus);
2603             final boolean becameInactive = (newStatus == PrinterInfo.STATUS_UNAVAILABLE
2604                     && oldStatus != newStatus);
2605 
2606             mPrinterAvailabilityDetector.updatePrinter(newPrinterState);
2607 
2608             mCurrentPrinter = newPrinterState;
2609 
2610             final boolean updateNeeded = ((capabChanged && hasCapab && isActive)
2611                     || (becameActive && hasCapab) || (isActive && gotCapab));
2612 
2613             if (capabChanged && hasCapab) {
2614                 updatePrintAttributesFromCapabilities(newCapab);
2615             }
2616 
2617             if (updateNeeded) {
2618                 updatePrintPreviewController(false);
2619             }
2620 
2621             if ((isActive && gotCapab) || (becameActive && hasCapab)) {
2622                 onPrinterAvailable(newPrinterState);
2623             } else if ((becameInactive && hadCabab) || (isActive && lostCapab)) {
2624                 onPrinterUnavailable(newPrinterState);
2625             }
2626 
2627             if (updateNeeded && canUpdateDocument()) {
2628                 updateDocument(false);
2629             }
2630 
2631             // Force a reload of the enabled print services to update mAdvancedPrintOptionsActivity
2632             // in onLoadFinished();
2633             getLoaderManager().getLoader(LOADER_ID_ENABLED_PRINT_SERVICES).forceLoad();
2634 
2635             updateOptionsUi();
2636             updateSummary();
2637         }
2638 
capabilitiesChanged(PrinterCapabilitiesInfo oldCapabilities, PrinterCapabilitiesInfo newCapabilities)2639         private boolean capabilitiesChanged(PrinterCapabilitiesInfo oldCapabilities,
2640                 PrinterCapabilitiesInfo newCapabilities) {
2641             if (oldCapabilities == null) {
2642                 if (newCapabilities != null) {
2643                     return true;
2644                 }
2645             } else if (!oldCapabilities.equals(newCapabilities)) {
2646                 return true;
2647             }
2648             return false;
2649         }
2650     }
2651 
2652     private final class MyOnItemSelectedListener implements AdapterView.OnItemSelectedListener {
2653         @Override
onItemSelected(AdapterView<?> spinner, View view, int position, long id)2654         public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) {
2655             boolean clearRanges = false;
2656 
2657             if (spinner == mDestinationSpinner) {
2658                 if (position == AdapterView.INVALID_POSITION) {
2659                     return;
2660                 }
2661 
2662                 if (id == DEST_ADAPTER_ITEM_ID_MORE) {
2663                     startSelectPrinterActivity();
2664                     return;
2665                 }
2666 
2667                 PrinterHolder currentItem = (PrinterHolder) mDestinationSpinner.getSelectedItem();
2668                 PrinterInfo currentPrinter = (currentItem != null) ? currentItem.printer : null;
2669 
2670                 // Why on earth item selected is called if no selection changed.
2671                 if (mCurrentPrinter == currentPrinter) {
2672                     return;
2673                 }
2674 
2675                 PrinterId oldId = null;
2676                 if (mCurrentPrinter != null) {
2677                     oldId = mCurrentPrinter.getId();
2678                 }
2679 
2680                 mCurrentPrinter = currentPrinter;
2681 
2682                 if (oldId != null) {
2683                     boolean printerRemoved = mDestinationSpinnerAdapter.pruneRemovedPrinter(oldId);
2684 
2685                     if (printerRemoved) {
2686                         // Trigger PrinterObserver.onChanged to adjust selection. This will call
2687                         // this function again.
2688                         mDestinationSpinnerAdapter.notifyDataSetChanged();
2689                         return;
2690                     }
2691                 }
2692 
2693                 PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
2694                         currentPrinter.getId());
2695                 if (!printerHolder.removed) {
2696                     setState(STATE_CONFIGURING);
2697                     ensurePreviewUiShown();
2698                 }
2699 
2700                 mPrintJob.setPrinterId(currentPrinter.getId());
2701                 mPrintJob.setPrinterName(currentPrinter.getName());
2702 
2703                 mPrinterRegistry.setTrackedPrinter(currentPrinter.getId());
2704 
2705                 PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities();
2706                 if (capabilities != null) {
2707                     updatePrintAttributesFromCapabilities(capabilities);
2708                 }
2709 
2710                 mPrinterAvailabilityDetector.updatePrinter(currentPrinter);
2711 
2712                 // Force a reload of the enabled print services to update
2713                 // mAdvancedPrintOptionsActivity in onLoadFinished();
2714                 getLoaderManager().getLoader(LOADER_ID_ENABLED_PRINT_SERVICES).forceLoad();
2715             } else if (spinner == mMediaSizeSpinner) {
2716                 SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position);
2717                 PrintAttributes attributes = mPrintJob.getAttributes();
2718 
2719                 MediaSize newMediaSize;
2720                 if (mOrientationSpinner.getSelectedItemPosition() == 0) {
2721                     newMediaSize = mediaItem.value.asPortrait();
2722                 } else {
2723                     newMediaSize = mediaItem.value.asLandscape();
2724                 }
2725 
2726                 if (newMediaSize != attributes.getMediaSize()) {
2727                     clearRanges = true;
2728                     attributes.setMediaSize(newMediaSize);
2729                 }
2730             } else if (spinner == mColorModeSpinner) {
2731                 SpinnerItem<Integer> colorModeItem = mColorModeSpinnerAdapter.getItem(position);
2732                 mPrintJob.getAttributes().setColorMode(colorModeItem.value);
2733             } else if (spinner == mDuplexModeSpinner) {
2734                 SpinnerItem<Integer> duplexModeItem = mDuplexModeSpinnerAdapter.getItem(position);
2735                 mPrintJob.getAttributes().setDuplexMode(duplexModeItem.value);
2736             } else if (spinner == mOrientationSpinner) {
2737                 SpinnerItem<Integer> orientationItem = mOrientationSpinnerAdapter.getItem(position);
2738                 PrintAttributes attributes = mPrintJob.getAttributes();
2739                 if (mMediaSizeSpinner.getSelectedItem() != null) {
2740                     boolean isPortrait = attributes.isPortrait();
2741 
2742                     if (isPortrait != (orientationItem.value == ORIENTATION_PORTRAIT)) {
2743                         clearRanges = true;
2744                         if (orientationItem.value == ORIENTATION_PORTRAIT) {
2745                             attributes.copyFrom(attributes.asPortrait());
2746                         } else {
2747                             attributes.copyFrom(attributes.asLandscape());
2748                         }
2749                     }
2750                 }
2751             } else if (spinner == mRangeOptionsSpinner) {
2752                 if (mRangeOptionsSpinner.getSelectedItemPosition() == 0) {
2753                     clearRanges = true;
2754                     mPageRangeEditText.setText("");
2755                 } else if (TextUtils.isEmpty(mPageRangeEditText.getText())) {
2756                     mPageRangeEditText.setError("");
2757                 }
2758             }
2759 
2760             if (clearRanges) {
2761                 clearPageRanges();
2762             }
2763 
2764             updateOptionsUi();
2765 
2766             if (canUpdateDocument()) {
2767                 updateDocument(false);
2768             }
2769         }
2770 
2771         @Override
onNothingSelected(AdapterView<?> parent)2772         public void onNothingSelected(AdapterView<?> parent) {
2773             /* do nothing*/
2774         }
2775     }
2776 
2777     private final class SelectAllOnFocusListener implements OnFocusChangeListener {
2778         @Override
onFocusChange(View view, boolean hasFocus)2779         public void onFocusChange(View view, boolean hasFocus) {
2780             EditText editText = (EditText) view;
2781             if (!TextUtils.isEmpty(editText.getText())) {
2782                 editText.setSelection(editText.getText().length());
2783             }
2784 
2785             if (view == mPageRangeEditText && !hasFocus && mPageRangeEditText.getError() == null) {
2786                 updateSelectedPagesFromTextField();
2787             }
2788         }
2789     }
2790 
2791     private final class RangeTextWatcher implements TextWatcher {
2792         @Override
onTextChanged(CharSequence s, int start, int before, int count)2793         public void onTextChanged(CharSequence s, int start, int before, int count) {
2794             /* do nothing */
2795         }
2796 
2797         @Override
beforeTextChanged(CharSequence s, int start, int count, int after)2798         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2799             /* do nothing */
2800         }
2801 
2802         @Override
afterTextChanged(Editable editable)2803         public void afterTextChanged(Editable editable) {
2804             final boolean hadErrors = hasErrors();
2805 
2806             PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
2807             final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
2808             PageRange[] ranges = PageRangeUtils.parsePageRanges(editable, pageCount);
2809 
2810             if (ranges.length == 0) {
2811                 if (mPageRangeEditText.getError() == null) {
2812                     mPageRangeEditText.setError("");
2813                     updateOptionsUi();
2814                 }
2815                 return;
2816             }
2817 
2818             if (mPageRangeEditText.getError() != null) {
2819                 mPageRangeEditText.setError(null);
2820                 updateOptionsUi();
2821             }
2822 
2823             if (hadErrors && canUpdateDocument()) {
2824                 updateDocument(false);
2825             }
2826         }
2827     }
2828 
2829     private final class EditTextWatcher implements TextWatcher {
2830         @Override
onTextChanged(CharSequence s, int start, int before, int count)2831         public void onTextChanged(CharSequence s, int start, int before, int count) {
2832             /* do nothing */
2833         }
2834 
2835         @Override
beforeTextChanged(CharSequence s, int start, int count, int after)2836         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2837             /* do nothing */
2838         }
2839 
2840         @Override
afterTextChanged(Editable editable)2841         public void afterTextChanged(Editable editable) {
2842             final boolean hadErrors = hasErrors();
2843 
2844             if (editable.length() == 0) {
2845                 if (mCopiesEditText.getError() == null) {
2846                     mCopiesEditText.setError("");
2847                     updateOptionsUi();
2848                 }
2849                 return;
2850             }
2851 
2852             int copies = 0;
2853             try {
2854                 copies = Integer.parseInt(editable.toString());
2855             } catch (NumberFormatException nfe) {
2856                 /* ignore */
2857             }
2858 
2859             if (copies < MIN_COPIES) {
2860                 if (mCopiesEditText.getError() == null) {
2861                     mCopiesEditText.setError("");
2862                     updateOptionsUi();
2863                 }
2864                 return;
2865             }
2866 
2867             mPrintJob.setCopies(copies);
2868 
2869             if (mCopiesEditText.getError() != null) {
2870                 mCopiesEditText.setError(null);
2871                 updateOptionsUi();
2872             }
2873 
2874             if (hadErrors && canUpdateDocument()) {
2875                 updateDocument(false);
2876             }
2877         }
2878     }
2879 
2880     private final class ProgressMessageController implements Runnable {
2881         private static final long PROGRESS_TIMEOUT_MILLIS = 1000;
2882 
2883         private final Handler mHandler;
2884 
2885         private boolean mPosted;
2886 
2887         /** State before run was executed */
2888         private int mPreviousState = -1;
2889 
ProgressMessageController(Context context)2890         public ProgressMessageController(Context context) {
2891             mHandler = new Handler(context.getMainLooper(), null, false);
2892         }
2893 
post()2894         public void post() {
2895             if (mState == STATE_UPDATE_SLOW) {
2896                 setState(STATE_UPDATE_SLOW);
2897                 ensureProgressUiShown();
2898                 updateOptionsUi();
2899 
2900                 return;
2901             } else if (mPosted) {
2902                 return;
2903             }
2904             mPreviousState = -1;
2905             mPosted = true;
2906             mHandler.postDelayed(this, PROGRESS_TIMEOUT_MILLIS);
2907         }
2908 
getStateAfterCancel()2909         private int getStateAfterCancel() {
2910             if (mPreviousState == -1) {
2911                 return mState;
2912             } else {
2913                 return mPreviousState;
2914             }
2915         }
2916 
cancel()2917         public int cancel() {
2918             int state;
2919 
2920             if (!mPosted) {
2921                 state = getStateAfterCancel();
2922             } else {
2923                 mPosted = false;
2924                 mHandler.removeCallbacks(this);
2925 
2926                 state = getStateAfterCancel();
2927             }
2928 
2929             mPreviousState = -1;
2930 
2931             return state;
2932         }
2933 
2934         @Override
run()2935         public void run() {
2936             mPosted = false;
2937             mPreviousState = mState;
2938             setState(STATE_UPDATE_SLOW);
2939             ensureProgressUiShown();
2940             updateOptionsUi();
2941         }
2942     }
2943 
2944     private static final class DocumentTransformer implements ServiceConnection {
2945         private static final String TEMP_FILE_PREFIX = "print_job";
2946         private static final String TEMP_FILE_EXTENSION = ".pdf";
2947 
2948         private final Context mContext;
2949 
2950         private final MutexFileProvider mFileProvider;
2951 
2952         private final PrintJobInfo mPrintJob;
2953 
2954         private final PageRange[] mPagesToShred;
2955 
2956         private final PrintAttributes mAttributesToApply;
2957 
2958         private final Runnable mCallback;
2959 
DocumentTransformer(Context context, PrintJobInfo printJob, MutexFileProvider fileProvider, PrintAttributes attributes, Runnable callback)2960         public DocumentTransformer(Context context, PrintJobInfo printJob,
2961                 MutexFileProvider fileProvider, PrintAttributes attributes,
2962                 Runnable callback) {
2963             mContext = context;
2964             mPrintJob = printJob;
2965             mFileProvider = fileProvider;
2966             mCallback = callback;
2967             mPagesToShred = computePagesToShred(mPrintJob);
2968             mAttributesToApply = attributes;
2969         }
2970 
transform()2971         public void transform() {
2972             // If we have only the pages we want, done.
2973             if (mPagesToShred.length <= 0 && mAttributesToApply == null) {
2974                 mCallback.run();
2975                 return;
2976             }
2977 
2978             // Bind to the manipulation service and the work
2979             // will be performed upon connection to the service.
2980             Intent intent = new Intent(PdfManipulationService.ACTION_GET_EDITOR);
2981             intent.setClass(mContext, PdfManipulationService.class);
2982             mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
2983         }
2984 
2985         @Override
onServiceConnected(ComponentName name, IBinder service)2986         public void onServiceConnected(ComponentName name, IBinder service) {
2987             final IPdfEditor editor = IPdfEditor.Stub.asInterface(service);
2988             new AsyncTask<Void, Void, Void>() {
2989                 @Override
2990                 protected Void doInBackground(Void... params) {
2991                     // It's OK to access the data members as they are
2992                     // final and this code is the last one to touch
2993                     // them as shredding is the very last step, so the
2994                     // UI is not interactive at this point.
2995                     doTransform(editor);
2996                     updatePrintJob();
2997                     return null;
2998                 }
2999 
3000                 @Override
3001                 protected void onPostExecute(Void aVoid) {
3002                     mContext.unbindService(DocumentTransformer.this);
3003                     mCallback.run();
3004                 }
3005             }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
3006         }
3007 
3008         @Override
onServiceDisconnected(ComponentName name)3009         public void onServiceDisconnected(ComponentName name) {
3010             /* do nothing */
3011         }
3012 
doTransform(IPdfEditor editor)3013         private void doTransform(IPdfEditor editor) {
3014             File tempFile = null;
3015             ParcelFileDescriptor src = null;
3016             ParcelFileDescriptor dst = null;
3017             InputStream in = null;
3018             OutputStream out = null;
3019             try {
3020                 File jobFile = mFileProvider.acquireFile(null);
3021                 src = ParcelFileDescriptor.open(jobFile, ParcelFileDescriptor.MODE_READ_WRITE);
3022 
3023                 // Open the document.
3024                 editor.openDocument(src);
3025 
3026                 // We passed the fd over IPC, close this one.
3027                 src.close();
3028 
3029                 // Drop the pages.
3030                 editor.removePages(mPagesToShred);
3031 
3032                 // Apply print attributes if needed.
3033                 if (mAttributesToApply != null) {
3034                     editor.applyPrintAttributes(mAttributesToApply);
3035                 }
3036 
3037                 // Write the modified PDF to a temp file.
3038                 tempFile = File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_EXTENSION,
3039                         mContext.getCacheDir());
3040                 dst = ParcelFileDescriptor.open(tempFile, ParcelFileDescriptor.MODE_READ_WRITE);
3041                 editor.write(dst);
3042                 dst.close();
3043 
3044                 // Close the document.
3045                 editor.closeDocument();
3046 
3047                 // Copy the temp file over the print job file.
3048                 jobFile.delete();
3049                 in = new FileInputStream(tempFile);
3050                 out = new FileOutputStream(jobFile);
3051                 Streams.copy(in, out);
3052             } catch (IOException|RemoteException e) {
3053                 Log.e(LOG_TAG, "Error dropping pages", e);
3054             } finally {
3055                 IoUtils.closeQuietly(src);
3056                 IoUtils.closeQuietly(dst);
3057                 IoUtils.closeQuietly(in);
3058                 IoUtils.closeQuietly(out);
3059                 if (tempFile != null) {
3060                     tempFile.delete();
3061                 }
3062                 mFileProvider.releaseFile();
3063             }
3064         }
3065 
updatePrintJob()3066         private void updatePrintJob() {
3067             // Update the print job pages.
3068             final int newPageCount = PageRangeUtils.getNormalizedPageCount(
3069                     mPrintJob.getPages(), 0);
3070             mPrintJob.setPages(new PageRange[]{PageRange.ALL_PAGES});
3071 
3072             // Update the print job document info.
3073             PrintDocumentInfo oldDocInfo = mPrintJob.getDocumentInfo();
3074             PrintDocumentInfo newDocInfo = new PrintDocumentInfo
3075                     .Builder(oldDocInfo.getName())
3076                     .setContentType(oldDocInfo.getContentType())
3077                     .setPageCount(newPageCount)
3078                     .build();
3079             mPrintJob.setDocumentInfo(newDocInfo);
3080         }
3081 
computePagesToShred(PrintJobInfo printJob)3082         private static PageRange[] computePagesToShred(PrintJobInfo printJob) {
3083             List<PageRange> rangesToShred = new ArrayList<>();
3084             PageRange previousRange = null;
3085 
3086             PageRange[] printedPages = printJob.getPages();
3087             final int rangeCount = printedPages.length;
3088             for (int i = 0; i < rangeCount; i++) {
3089                 PageRange range = printedPages[i];
3090 
3091                 if (previousRange == null) {
3092                     final int startPageIdx = 0;
3093                     final int endPageIdx = range.getStart() - 1;
3094                     if (startPageIdx <= endPageIdx) {
3095                         PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
3096                         rangesToShred.add(removedRange);
3097                     }
3098                 } else {
3099                     final int startPageIdx = previousRange.getEnd() + 1;
3100                     final int endPageIdx = range.getStart() - 1;
3101                     if (startPageIdx <= endPageIdx) {
3102                         PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
3103                         rangesToShred.add(removedRange);
3104                     }
3105                 }
3106 
3107                 if (i == rangeCount - 1) {
3108                     if (range.getEnd() != Integer.MAX_VALUE) {
3109                         rangesToShred.add(new PageRange(range.getEnd() + 1, Integer.MAX_VALUE));
3110                     }
3111                 }
3112 
3113                 previousRange = range;
3114             }
3115 
3116             PageRange[] result = new PageRange[rangesToShred.size()];
3117             rangesToShred.toArray(result);
3118             return result;
3119         }
3120     }
3121 }
3122