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