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