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