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