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