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