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