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