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