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