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