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