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