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