PrintActivity.java revision f24285d7b323ff7dd5b3f2f4d9910db2c9a6495d
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        mPageRangeTitle.setVisibility(View.INVISIBLE);
1271        mPageRangeEditText.setOnFocusChangeListener(mSelectAllOnFocusListener);
1272        mPageRangeEditText.addTextChangedListener(new RangeTextWatcher());
1273
1274        // Advanced options button.
1275        mMoreOptionsButton = (Button) findViewById(R.id.more_options_button);
1276        mMoreOptionsButton.setOnClickListener(clickListener);
1277
1278        // Print button
1279        mPrintButton = (ImageView) findViewById(R.id.print_button);
1280        mPrintButton.setOnClickListener(clickListener);
1281
1282        // The UI is now initialized
1283        mIsOptionsUiBound = true;
1284
1285        // Special prompt instead of destination spinner for the first time the user printed
1286        if (!hasUserEverPrinted()) {
1287            mShowDestinationPrompt = true;
1288
1289            mSummaryCopies.setEnabled(false);
1290            mSummaryPaperSize.setEnabled(false);
1291
1292            mDestinationSpinner.setOnTouchListener(new View.OnTouchListener() {
1293                @Override
1294                public boolean onTouch(View v, MotionEvent event) {
1295                    mShowDestinationPrompt = false;
1296                    mSummaryCopies.setEnabled(true);
1297                    mSummaryPaperSize.setEnabled(true);
1298                    updateOptionsUi();
1299
1300                    mDestinationSpinner.setOnTouchListener(null);
1301                    mDestinationSpinnerAdapter.notifyDataSetChanged();
1302
1303                    return false;
1304                }
1305            });
1306        }
1307    }
1308
1309    @Override
1310    public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) {
1311        return new PrintServicesLoader((PrintManager) getSystemService(Context.PRINT_SERVICE), this,
1312                PrintManager.ENABLED_SERVICES);
1313    }
1314
1315    @Override
1316    public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
1317            List<PrintServiceInfo> services) {
1318        ComponentName newAdvancedPrintOptionsActivity = null;
1319        if (mCurrentPrinter != null && services != null) {
1320            final int numServices = services.size();
1321            for (int i = 0; i < numServices; i++) {
1322                PrintServiceInfo service = services.get(i);
1323
1324                if (service.getComponentName().equals(mCurrentPrinter.getId().getServiceName())) {
1325                    String advancedOptionsActivityName = service.getAdvancedOptionsActivityName();
1326
1327                    if (!TextUtils.isEmpty(advancedOptionsActivityName)) {
1328                        newAdvancedPrintOptionsActivity = new ComponentName(
1329                                service.getComponentName().getPackageName(),
1330                                advancedOptionsActivityName);
1331
1332                        break;
1333                    }
1334                }
1335            }
1336        }
1337
1338        if (!Objects.equals(newAdvancedPrintOptionsActivity, mAdvancedPrintOptionsActivity)) {
1339            mAdvancedPrintOptionsActivity = newAdvancedPrintOptionsActivity;
1340            updateOptionsUi();
1341        }
1342
1343        boolean newArePrintServicesEnabled = services != null && !services.isEmpty();
1344        if (mArePrintServicesEnabled != newArePrintServicesEnabled) {
1345            mArePrintServicesEnabled = newArePrintServicesEnabled;
1346
1347            // Reload mDestinationSpinnerAdapter as mArePrintServicesEnabled changed and the adapter
1348            // reads that in DestinationAdapter#getMoreItemTitle
1349            if (mDestinationSpinnerAdapter != null) {
1350                mDestinationSpinnerAdapter.notifyDataSetChanged();
1351            }
1352        }
1353    }
1354
1355    @Override
1356    public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) {
1357        if (!isFinishing()) {
1358            onLoadFinished(loader, null);
1359        }
1360    }
1361
1362    /**
1363     * A dialog that asks the user to approve a {@link PrintService}. This dialog is automatically
1364     * dismissed if the same {@link PrintService} gets approved by another
1365     * {@link PrintServiceApprovalDialog}.
1366     */
1367    private static final class PrintServiceApprovalDialog extends DialogFragment
1368            implements OnSharedPreferenceChangeListener {
1369        private static final String PRINTSERVICE_KEY = "PRINTSERVICE";
1370        private ApprovedPrintServices mApprovedServices;
1371
1372        /**
1373         * Create a new {@link PrintServiceApprovalDialog} that ask the user to approve a
1374         * {@link PrintService}.
1375         *
1376         * @param printService The {@link ComponentName} of the service to approve
1377         * @return A new {@link PrintServiceApprovalDialog} that might approve the service
1378         */
1379        static PrintServiceApprovalDialog newInstance(ComponentName printService) {
1380            PrintServiceApprovalDialog dialog = new PrintServiceApprovalDialog();
1381
1382            Bundle args = new Bundle();
1383            args.putParcelable(PRINTSERVICE_KEY, printService);
1384            dialog.setArguments(args);
1385
1386            return dialog;
1387        }
1388
1389        @Override
1390        public void onStop() {
1391            super.onStop();
1392
1393            mApprovedServices.unregisterChangeListener(this);
1394        }
1395
1396        @Override
1397        public void onStart() {
1398            super.onStart();
1399
1400            ComponentName printService = getArguments().getParcelable(PRINTSERVICE_KEY);
1401            synchronized (ApprovedPrintServices.sLock) {
1402                if (mApprovedServices.isApprovedService(printService)) {
1403                    dismiss();
1404                } else {
1405                    mApprovedServices.registerChangeListenerLocked(this);
1406                }
1407            }
1408        }
1409
1410        @Override
1411        public Dialog onCreateDialog(Bundle savedInstanceState) {
1412            super.onCreateDialog(savedInstanceState);
1413
1414            mApprovedServices = new ApprovedPrintServices(getActivity());
1415
1416            PackageManager packageManager = getActivity().getPackageManager();
1417            CharSequence serviceLabel;
1418            try {
1419                ComponentName printService = getArguments().getParcelable(PRINTSERVICE_KEY);
1420
1421                serviceLabel = packageManager.getApplicationInfo(printService.getPackageName(), 0)
1422                        .loadLabel(packageManager);
1423            } catch (NameNotFoundException e) {
1424                serviceLabel = null;
1425            }
1426
1427            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
1428            builder.setTitle(getString(R.string.print_service_security_warning_title,
1429                    serviceLabel))
1430                    .setMessage(getString(R.string.print_service_security_warning_summary,
1431                            serviceLabel))
1432                    .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
1433                        @Override
1434                        public void onClick(DialogInterface dialog, int id) {
1435                            ComponentName printService =
1436                                    getArguments().getParcelable(PRINTSERVICE_KEY);
1437                            // Prevent onSharedPreferenceChanged from getting triggered
1438                            mApprovedServices
1439                                    .unregisterChangeListener(PrintServiceApprovalDialog.this);
1440
1441                            mApprovedServices.addApprovedService(printService);
1442                            ((PrintActivity) getActivity()).confirmPrint();
1443                        }
1444                    })
1445                    .setNegativeButton(android.R.string.cancel, null);
1446
1447            return builder.create();
1448        }
1449
1450        @Override
1451        public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
1452            ComponentName printService = getArguments().getParcelable(PRINTSERVICE_KEY);
1453
1454            synchronized (ApprovedPrintServices.sLock) {
1455                if (mApprovedServices.isApprovedService(printService)) {
1456                    dismiss();
1457                }
1458            }
1459        }
1460    }
1461
1462    private final class MyClickListener implements OnClickListener {
1463        @Override
1464        public void onClick(View view) {
1465            if (view == mPrintButton) {
1466                if (mCurrentPrinter != null) {
1467                    if (mDestinationSpinnerAdapter.getPdfPrinter() == mCurrentPrinter) {
1468                        confirmPrint();
1469                    } else {
1470                        ApprovedPrintServices approvedServices =
1471                                new ApprovedPrintServices(PrintActivity.this);
1472
1473                        ComponentName printService = mCurrentPrinter.getId().getServiceName();
1474                        if (approvedServices.isApprovedService(printService)) {
1475                            confirmPrint();
1476                        } else {
1477                            PrintServiceApprovalDialog.newInstance(printService)
1478                                    .show(getFragmentManager(), "approve");
1479                        }
1480                    }
1481                } else {
1482                    cancelPrint();
1483                }
1484            } else if (view == mMoreOptionsButton) {
1485                if (mPageRangeEditText.getError() == null) {
1486                    // The selected pages is only applied once the user leaves the text field. A click
1487                    // on this button, does not count as leaving.
1488                    updateSelectedPagesFromTextField();
1489                }
1490
1491                if (mCurrentPrinter != null) {
1492                    startAdvancedPrintOptionsActivity(mCurrentPrinter);
1493                }
1494            }
1495        }
1496    }
1497
1498    private static boolean canPrint(PrinterInfo printer) {
1499        return printer.getCapabilities() != null
1500                && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
1501    }
1502
1503    /**
1504     * Disable all options UI elements, beside the {@link #mDestinationSpinner}
1505     */
1506    private void disableOptionsUi() {
1507        mCopiesEditText.setEnabled(false);
1508        mCopiesEditText.setFocusable(false);
1509        mMediaSizeSpinner.setEnabled(false);
1510        mColorModeSpinner.setEnabled(false);
1511        mDuplexModeSpinner.setEnabled(false);
1512        mOrientationSpinner.setEnabled(false);
1513        mRangeOptionsSpinner.setEnabled(false);
1514        mPageRangeEditText.setEnabled(false);
1515        mPrintButton.setVisibility(View.GONE);
1516        mMoreOptionsButton.setEnabled(false);
1517    }
1518
1519    void updateOptionsUi() {
1520        if (!mIsOptionsUiBound) {
1521            return;
1522        }
1523
1524        // Always update the summary.
1525        updateSummary();
1526
1527        if (mState == STATE_PRINT_CONFIRMED
1528                || mState == STATE_PRINT_COMPLETED
1529                || mState == STATE_PRINT_CANCELED
1530                || mState == STATE_UPDATE_FAILED
1531                || mState == STATE_CREATE_FILE_FAILED
1532                || mState == STATE_PRINTER_UNAVAILABLE
1533                || mState == STATE_UPDATE_SLOW) {
1534            if (mState != STATE_PRINTER_UNAVAILABLE) {
1535                mDestinationSpinner.setEnabled(false);
1536            }
1537            disableOptionsUi();
1538            return;
1539        }
1540
1541        // If no current printer, or it has no capabilities, or it is not
1542        // available, we disable all print options except the destination.
1543        if (mCurrentPrinter == null || !canPrint(mCurrentPrinter)) {
1544            disableOptionsUi();
1545            return;
1546        }
1547
1548        PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
1549        PrintAttributes defaultAttributes = capabilities.getDefaults();
1550
1551        // Destination.
1552        mDestinationSpinner.setEnabled(true);
1553
1554        // Media size.
1555        mMediaSizeSpinner.setEnabled(true);
1556
1557        List<MediaSize> mediaSizes = new ArrayList<>(capabilities.getMediaSizes());
1558        // Sort the media sizes based on the current locale.
1559        Collections.sort(mediaSizes, mMediaSizeComparator);
1560
1561        PrintAttributes attributes = mPrintJob.getAttributes();
1562
1563        // If the media sizes changed, we update the adapter and the spinner.
1564        boolean mediaSizesChanged = false;
1565        final int mediaSizeCount = mediaSizes.size();
1566        if (mediaSizeCount != mMediaSizeSpinnerAdapter.getCount()) {
1567            mediaSizesChanged = true;
1568        } else {
1569            for (int i = 0; i < mediaSizeCount; i++) {
1570                if (!mediaSizes.get(i).equals(mMediaSizeSpinnerAdapter.getItem(i).value)) {
1571                    mediaSizesChanged = true;
1572                    break;
1573                }
1574            }
1575        }
1576        if (mediaSizesChanged) {
1577            // Remember the old media size to try selecting it again.
1578            int oldMediaSizeNewIndex = AdapterView.INVALID_POSITION;
1579            MediaSize oldMediaSize = attributes.getMediaSize();
1580
1581            // Rebuild the adapter data.
1582            mMediaSizeSpinnerAdapter.clear();
1583            for (int i = 0; i < mediaSizeCount; i++) {
1584                MediaSize mediaSize = mediaSizes.get(i);
1585                if (oldMediaSize != null
1586                        && mediaSize.asPortrait().equals(oldMediaSize.asPortrait())) {
1587                    // Update the index of the old selection.
1588                    oldMediaSizeNewIndex = i;
1589                }
1590                mMediaSizeSpinnerAdapter.add(new SpinnerItem<>(
1591                        mediaSize, mediaSize.getLabel(getPackageManager())));
1592            }
1593
1594            if (oldMediaSizeNewIndex != AdapterView.INVALID_POSITION) {
1595                // Select the old media size - nothing really changed.
1596                if (mMediaSizeSpinner.getSelectedItemPosition() != oldMediaSizeNewIndex) {
1597                    mMediaSizeSpinner.setSelection(oldMediaSizeNewIndex);
1598                }
1599            } else {
1600                // Select the first or the default.
1601                final int mediaSizeIndex = Math.max(mediaSizes.indexOf(
1602                        defaultAttributes.getMediaSize()), 0);
1603                if (mMediaSizeSpinner.getSelectedItemPosition() != mediaSizeIndex) {
1604                    mMediaSizeSpinner.setSelection(mediaSizeIndex);
1605                }
1606                // Respect the orientation of the old selection.
1607                if (oldMediaSize != null) {
1608                    if (oldMediaSize.isPortrait()) {
1609                        attributes.setMediaSize(mMediaSizeSpinnerAdapter
1610                                .getItem(mediaSizeIndex).value.asPortrait());
1611                    } else {
1612                        attributes.setMediaSize(mMediaSizeSpinnerAdapter
1613                                .getItem(mediaSizeIndex).value.asLandscape());
1614                    }
1615                }
1616            }
1617        }
1618
1619        // Color mode.
1620        mColorModeSpinner.setEnabled(true);
1621        final int colorModes = capabilities.getColorModes();
1622
1623        // If the color modes changed, we update the adapter and the spinner.
1624        boolean colorModesChanged = false;
1625        if (Integer.bitCount(colorModes) != mColorModeSpinnerAdapter.getCount()) {
1626            colorModesChanged = true;
1627        } else {
1628            int remainingColorModes = colorModes;
1629            int adapterIndex = 0;
1630            while (remainingColorModes != 0) {
1631                final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
1632                final int colorMode = 1 << colorBitOffset;
1633                remainingColorModes &= ~colorMode;
1634                if (colorMode != mColorModeSpinnerAdapter.getItem(adapterIndex).value) {
1635                    colorModesChanged = true;
1636                    break;
1637                }
1638                adapterIndex++;
1639            }
1640        }
1641        if (colorModesChanged) {
1642            // Remember the old color mode to try selecting it again.
1643            int oldColorModeNewIndex = AdapterView.INVALID_POSITION;
1644            final int oldColorMode = attributes.getColorMode();
1645
1646            // Rebuild the adapter data.
1647            mColorModeSpinnerAdapter.clear();
1648            String[] colorModeLabels = getResources().getStringArray(R.array.color_mode_labels);
1649            int remainingColorModes = colorModes;
1650            while (remainingColorModes != 0) {
1651                final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
1652                final int colorMode = 1 << colorBitOffset;
1653                if (colorMode == oldColorMode) {
1654                    // Update the index of the old selection.
1655                    oldColorModeNewIndex = mColorModeSpinnerAdapter.getCount();
1656                }
1657                remainingColorModes &= ~colorMode;
1658                mColorModeSpinnerAdapter.add(new SpinnerItem<>(colorMode,
1659                        colorModeLabels[colorBitOffset]));
1660            }
1661            if (oldColorModeNewIndex != AdapterView.INVALID_POSITION) {
1662                // Select the old color mode - nothing really changed.
1663                if (mColorModeSpinner.getSelectedItemPosition() != oldColorModeNewIndex) {
1664                    mColorModeSpinner.setSelection(oldColorModeNewIndex);
1665                }
1666            } else {
1667                // Select the default.
1668                final int selectedColorMode = colorModes & defaultAttributes.getColorMode();
1669                final int itemCount = mColorModeSpinnerAdapter.getCount();
1670                for (int i = 0; i < itemCount; i++) {
1671                    SpinnerItem<Integer> item = mColorModeSpinnerAdapter.getItem(i);
1672                    if (selectedColorMode == item.value) {
1673                        if (mColorModeSpinner.getSelectedItemPosition() != i) {
1674                            mColorModeSpinner.setSelection(i);
1675                        }
1676                        attributes.setColorMode(selectedColorMode);
1677                        break;
1678                    }
1679                }
1680            }
1681        }
1682
1683        // Duplex mode.
1684        mDuplexModeSpinner.setEnabled(true);
1685        final int duplexModes = capabilities.getDuplexModes();
1686
1687        // If the duplex modes changed, we update the adapter and the spinner.
1688        // Note that we use bit count +1 to account for the no duplex option.
1689        boolean duplexModesChanged = false;
1690        if (Integer.bitCount(duplexModes) != mDuplexModeSpinnerAdapter.getCount()) {
1691            duplexModesChanged = true;
1692        } else {
1693            int remainingDuplexModes = duplexModes;
1694            int adapterIndex = 0;
1695            while (remainingDuplexModes != 0) {
1696                final int duplexBitOffset = Integer.numberOfTrailingZeros(remainingDuplexModes);
1697                final int duplexMode = 1 << duplexBitOffset;
1698                remainingDuplexModes &= ~duplexMode;
1699                if (duplexMode != mDuplexModeSpinnerAdapter.getItem(adapterIndex).value) {
1700                    duplexModesChanged = true;
1701                    break;
1702                }
1703                adapterIndex++;
1704            }
1705        }
1706        if (duplexModesChanged) {
1707            // Remember the old duplex mode to try selecting it again. Also the fallback
1708            // is no duplexing which is always the first item in the dropdown.
1709            int oldDuplexModeNewIndex = AdapterView.INVALID_POSITION;
1710            final int oldDuplexMode = attributes.getDuplexMode();
1711
1712            // Rebuild the adapter data.
1713            mDuplexModeSpinnerAdapter.clear();
1714            String[] duplexModeLabels = getResources().getStringArray(R.array.duplex_mode_labels);
1715            int remainingDuplexModes = duplexModes;
1716            while (remainingDuplexModes != 0) {
1717                final int duplexBitOffset = Integer.numberOfTrailingZeros(remainingDuplexModes);
1718                final int duplexMode = 1 << duplexBitOffset;
1719                if (duplexMode == oldDuplexMode) {
1720                    // Update the index of the old selection.
1721                    oldDuplexModeNewIndex = mDuplexModeSpinnerAdapter.getCount();
1722                }
1723                remainingDuplexModes &= ~duplexMode;
1724                mDuplexModeSpinnerAdapter.add(new SpinnerItem<>(duplexMode,
1725                        duplexModeLabels[duplexBitOffset]));
1726            }
1727
1728            if (oldDuplexModeNewIndex != AdapterView.INVALID_POSITION) {
1729                // Select the old duplex mode - nothing really changed.
1730                if (mDuplexModeSpinner.getSelectedItemPosition() != oldDuplexModeNewIndex) {
1731                    mDuplexModeSpinner.setSelection(oldDuplexModeNewIndex);
1732                }
1733            } else {
1734                // Select the default.
1735                final int selectedDuplexMode = defaultAttributes.getDuplexMode();
1736                final int itemCount = mDuplexModeSpinnerAdapter.getCount();
1737                for (int i = 0; i < itemCount; i++) {
1738                    SpinnerItem<Integer> item = mDuplexModeSpinnerAdapter.getItem(i);
1739                    if (selectedDuplexMode == item.value) {
1740                        if (mDuplexModeSpinner.getSelectedItemPosition() != i) {
1741                            mDuplexModeSpinner.setSelection(i);
1742                        }
1743                        attributes.setDuplexMode(selectedDuplexMode);
1744                        break;
1745                    }
1746                }
1747            }
1748        }
1749
1750        mDuplexModeSpinner.setEnabled(mDuplexModeSpinnerAdapter.getCount() > 1);
1751
1752        // Orientation
1753        mOrientationSpinner.setEnabled(true);
1754        MediaSize mediaSize = attributes.getMediaSize();
1755        if (mediaSize != null) {
1756            if (mediaSize.isPortrait()
1757                    && mOrientationSpinner.getSelectedItemPosition() != 0) {
1758                mOrientationSpinner.setSelection(0);
1759            } else if (!mediaSize.isPortrait()
1760                    && mOrientationSpinner.getSelectedItemPosition() != 1) {
1761                mOrientationSpinner.setSelection(1);
1762            }
1763        }
1764
1765        // Range options
1766        PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
1767        final int pageCount = getAdjustedPageCount(info);
1768        if (pageCount > 0) {
1769            if (info != null) {
1770                if (pageCount == 1) {
1771                    mRangeOptionsSpinner.setEnabled(false);
1772                } else {
1773                    mRangeOptionsSpinner.setEnabled(true);
1774                    if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
1775                        if (!mPageRangeEditText.isEnabled()) {
1776                            mPageRangeEditText.setEnabled(true);
1777                            mPageRangeEditText.setVisibility(View.VISIBLE);
1778                            mPageRangeTitle.setVisibility(View.VISIBLE);
1779                            mPageRangeEditText.requestFocus();
1780                            InputMethodManager imm = (InputMethodManager)
1781                                    getSystemService(Context.INPUT_METHOD_SERVICE);
1782                            imm.showSoftInput(mPageRangeEditText, 0);
1783                        }
1784                    } else {
1785                        mPageRangeEditText.setEnabled(false);
1786                        mPageRangeEditText.setVisibility(View.INVISIBLE);
1787                        mPageRangeTitle.setVisibility(View.INVISIBLE);
1788                    }
1789                }
1790            } else {
1791                if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
1792                    mRangeOptionsSpinner.setSelection(0);
1793                    mPageRangeEditText.setText("");
1794                }
1795                mRangeOptionsSpinner.setEnabled(false);
1796                mPageRangeEditText.setEnabled(false);
1797                mPageRangeEditText.setVisibility(View.INVISIBLE);
1798                mPageRangeTitle.setVisibility(View.INVISIBLE);
1799            }
1800        }
1801
1802        final int newPageCount = getAdjustedPageCount(info);
1803        if (newPageCount != mCurrentPageCount) {
1804            mCurrentPageCount = newPageCount;
1805            updatePageRangeOptions(newPageCount);
1806        }
1807
1808        // Advanced print options
1809        if (mAdvancedPrintOptionsActivity != null) {
1810            mMoreOptionsButton.setVisibility(View.VISIBLE);
1811            mMoreOptionsButton.setEnabled(true);
1812        } else {
1813            mMoreOptionsButton.setVisibility(View.GONE);
1814            mMoreOptionsButton.setEnabled(false);
1815        }
1816
1817        // Print
1818        if (mDestinationSpinnerAdapter.getPdfPrinter() != mCurrentPrinter) {
1819            mPrintButton.setImageResource(com.android.internal.R.drawable.ic_print);
1820            mPrintButton.setContentDescription(getString(R.string.print_button));
1821        } else {
1822            mPrintButton.setImageResource(R.drawable.ic_menu_savetopdf);
1823            mPrintButton.setContentDescription(getString(R.string.savetopdf_button));
1824        }
1825        if (!mPrintedDocument.getDocumentInfo().laidout
1826                ||(mRangeOptionsSpinner.getSelectedItemPosition() == 1
1827                && (TextUtils.isEmpty(mPageRangeEditText.getText()) || hasErrors()))
1828                || (mRangeOptionsSpinner.getSelectedItemPosition() == 0
1829                && (mPrintedDocument.getDocumentInfo() == null || hasErrors()))) {
1830            mPrintButton.setVisibility(View.GONE);
1831        } else {
1832            mPrintButton.setVisibility(View.VISIBLE);
1833        }
1834
1835        // Copies
1836        if (mDestinationSpinnerAdapter.getPdfPrinter() != mCurrentPrinter) {
1837            mCopiesEditText.setEnabled(true);
1838            mCopiesEditText.setFocusableInTouchMode(true);
1839        } else {
1840            CharSequence text = mCopiesEditText.getText();
1841            if (TextUtils.isEmpty(text) || !MIN_COPIES_STRING.equals(text.toString())) {
1842                mCopiesEditText.setText(MIN_COPIES_STRING);
1843            }
1844            mCopiesEditText.setEnabled(false);
1845            mCopiesEditText.setFocusable(false);
1846        }
1847        if (mCopiesEditText.getError() == null
1848                && TextUtils.isEmpty(mCopiesEditText.getText())) {
1849            mCopiesEditText.setText(MIN_COPIES_STRING);
1850            mCopiesEditText.requestFocus();
1851        }
1852
1853        if (mShowDestinationPrompt) {
1854            disableOptionsUi();
1855        }
1856    }
1857
1858    private void updateSummary() {
1859        if (!mIsOptionsUiBound) {
1860            return;
1861        }
1862
1863        CharSequence copiesText = null;
1864        CharSequence mediaSizeText = null;
1865
1866        if (!TextUtils.isEmpty(mCopiesEditText.getText())) {
1867            copiesText = mCopiesEditText.getText();
1868            mSummaryCopies.setText(copiesText);
1869        }
1870
1871        final int selectedMediaIndex = mMediaSizeSpinner.getSelectedItemPosition();
1872        if (selectedMediaIndex >= 0) {
1873            SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(selectedMediaIndex);
1874            mediaSizeText = mediaItem.label;
1875            mSummaryPaperSize.setText(mediaSizeText);
1876        }
1877
1878        if (!TextUtils.isEmpty(copiesText) && !TextUtils.isEmpty(mediaSizeText)) {
1879            String summaryText = getString(R.string.summary_template, copiesText, mediaSizeText);
1880            mSummaryContainer.setContentDescription(summaryText);
1881        }
1882    }
1883
1884    private void updatePageRangeOptions(int pageCount) {
1885        @SuppressWarnings("unchecked")
1886        ArrayAdapter<SpinnerItem<Integer>> rangeOptionsSpinnerAdapter =
1887                (ArrayAdapter<SpinnerItem<Integer>>) mRangeOptionsSpinner.getAdapter();
1888        rangeOptionsSpinnerAdapter.clear();
1889
1890        final int[] rangeOptionsValues = getResources().getIntArray(
1891                R.array.page_options_values);
1892
1893        String pageCountLabel = (pageCount > 0) ? String.valueOf(pageCount) : "";
1894        String[] rangeOptionsLabels = new String[] {
1895            getString(R.string.template_all_pages, pageCountLabel),
1896            getString(R.string.template_page_range, pageCountLabel)
1897        };
1898
1899        final int rangeOptionsCount = rangeOptionsLabels.length;
1900        for (int i = 0; i < rangeOptionsCount; i++) {
1901            rangeOptionsSpinnerAdapter.add(new SpinnerItem<>(
1902                    rangeOptionsValues[i], rangeOptionsLabels[i]));
1903        }
1904    }
1905
1906    private PageRange[] computeSelectedPages() {
1907        if (hasErrors()) {
1908            return null;
1909        }
1910
1911        if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
1912            PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
1913            final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
1914
1915            return PageRangeUtils.parsePageRanges(mPageRangeEditText.getText(), pageCount);
1916        }
1917
1918        return PageRange.ALL_PAGES_ARRAY;
1919    }
1920
1921    private int getAdjustedPageCount(PrintDocumentInfo info) {
1922        if (info != null) {
1923            final int pageCount = info.getPageCount();
1924            if (pageCount != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) {
1925                return pageCount;
1926            }
1927        }
1928        // If the app does not tell us how many pages are in the
1929        // doc we ask for all pages and use the document page count.
1930        return mPrintPreviewController.getFilePageCount();
1931    }
1932
1933    private boolean hasErrors() {
1934        return (mCopiesEditText.getError() != null)
1935                || (mPageRangeEditText.getVisibility() == View.VISIBLE
1936                && mPageRangeEditText.getError() != null);
1937    }
1938
1939    public void onPrinterAvailable(PrinterInfo printer) {
1940        if (mCurrentPrinter.equals(printer)) {
1941            setState(STATE_CONFIGURING);
1942            if (canUpdateDocument()) {
1943                updateDocument(false);
1944            }
1945            ensurePreviewUiShown();
1946            updateOptionsUi();
1947        }
1948    }
1949
1950    public void onPrinterUnavailable(PrinterInfo printer) {
1951        if (mCurrentPrinter.getId().equals(printer.getId())) {
1952            setState(STATE_PRINTER_UNAVAILABLE);
1953            mPrintedDocument.cancel(false);
1954            ensureErrorUiShown(getString(R.string.print_error_printer_unavailable),
1955                    PrintErrorFragment.ACTION_NONE);
1956            updateOptionsUi();
1957        }
1958    }
1959
1960    private boolean canUpdateDocument() {
1961        if (mPrintedDocument.isDestroyed()) {
1962            return false;
1963        }
1964
1965        if (hasErrors()) {
1966            return false;
1967        }
1968
1969        PrintAttributes attributes = mPrintJob.getAttributes();
1970
1971        final int colorMode = attributes.getColorMode();
1972        if (colorMode != PrintAttributes.COLOR_MODE_COLOR
1973                && colorMode != PrintAttributes.COLOR_MODE_MONOCHROME) {
1974            return false;
1975        }
1976        if (attributes.getMediaSize() == null) {
1977            return false;
1978        }
1979        if (attributes.getMinMargins() == null) {
1980            return false;
1981        }
1982        if (attributes.getResolution() == null) {
1983            return false;
1984        }
1985
1986        if (mCurrentPrinter == null) {
1987            return false;
1988        }
1989        PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
1990        if (capabilities == null) {
1991            return false;
1992        }
1993        if (mCurrentPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE) {
1994            return false;
1995        }
1996
1997        return true;
1998    }
1999
2000    private void transformDocumentAndFinish(final Uri writeToUri) {
2001        // If saving to PDF, apply the attibutes as we are acting as a print service.
2002        PrintAttributes attributes = mDestinationSpinnerAdapter.getPdfPrinter() == mCurrentPrinter
2003                ?  mPrintJob.getAttributes() : null;
2004        new DocumentTransformer(this, mPrintJob, mFileProvider, attributes, new Runnable() {
2005            @Override
2006            public void run() {
2007                if (writeToUri != null) {
2008                    mPrintedDocument.writeContent(getContentResolver(), writeToUri);
2009                }
2010                setState(STATE_PRINT_COMPLETED);
2011                doFinish();
2012            }
2013        }).transform();
2014    }
2015
2016    private void doFinish() {
2017        if (mPrintedDocument != null && mPrintedDocument.isUpdating()) {
2018            // The printedDocument will call doFinish() when the current command finishes
2019            return;
2020        }
2021
2022        if (mIsFinishing) {
2023            return;
2024        }
2025
2026        mIsFinishing = true;
2027
2028        if (mPrinterRegistry != null) {
2029            mPrinterRegistry.setTrackedPrinter(null);
2030        }
2031
2032        if (mPrintersObserver != null) {
2033            mDestinationSpinnerAdapter.unregisterDataSetObserver(mPrintersObserver);
2034        }
2035
2036        if (mSpoolerProvider != null) {
2037            mSpoolerProvider.destroy();
2038        }
2039
2040        setState(mProgressMessageController.cancel());
2041
2042        if (mState != STATE_INITIALIZING) {
2043            mPrintedDocument.finish();
2044            mPrintedDocument.destroy();
2045            mPrintPreviewController.destroy(new Runnable() {
2046                @Override
2047                public void run() {
2048                    finish();
2049                }
2050            });
2051        } else {
2052            finish();
2053        }
2054    }
2055
2056    private final class SpinnerItem<T> {
2057        final T value;
2058        final CharSequence label;
2059
2060        public SpinnerItem(T value, CharSequence label) {
2061            this.value = value;
2062            this.label = label;
2063        }
2064
2065        @Override
2066        public String toString() {
2067            return label.toString();
2068        }
2069    }
2070
2071    private final class PrinterAvailabilityDetector implements Runnable {
2072        private static final long UNAVAILABLE_TIMEOUT_MILLIS = 10000; // 10sec
2073
2074        private boolean mPosted;
2075
2076        private boolean mPrinterUnavailable;
2077
2078        private PrinterInfo mPrinter;
2079
2080        public void updatePrinter(PrinterInfo printer) {
2081            if (printer.equals(mDestinationSpinnerAdapter.getPdfPrinter())) {
2082                return;
2083            }
2084
2085            final boolean available = printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE
2086                    && printer.getCapabilities() != null;
2087            final boolean notifyIfAvailable;
2088
2089            if (mPrinter == null || !mPrinter.getId().equals(printer.getId())) {
2090                notifyIfAvailable = true;
2091                unpostIfNeeded();
2092                mPrinterUnavailable = false;
2093                mPrinter = new PrinterInfo.Builder(printer).build();
2094            } else {
2095                notifyIfAvailable =
2096                        (mPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE
2097                                && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE)
2098                                || (mPrinter.getCapabilities() == null
2099                                && printer.getCapabilities() != null);
2100                mPrinter = printer;
2101            }
2102
2103            if (available) {
2104                unpostIfNeeded();
2105                mPrinterUnavailable = false;
2106                if (notifyIfAvailable) {
2107                    onPrinterAvailable(mPrinter);
2108                }
2109            } else {
2110                if (!mPrinterUnavailable) {
2111                    postIfNeeded();
2112                }
2113            }
2114        }
2115
2116        public void cancel() {
2117            unpostIfNeeded();
2118            mPrinterUnavailable = false;
2119        }
2120
2121        private void postIfNeeded() {
2122            if (!mPosted) {
2123                mPosted = true;
2124                mDestinationSpinner.postDelayed(this, UNAVAILABLE_TIMEOUT_MILLIS);
2125            }
2126        }
2127
2128        private void unpostIfNeeded() {
2129            if (mPosted) {
2130                mPosted = false;
2131                mDestinationSpinner.removeCallbacks(this);
2132            }
2133        }
2134
2135        @Override
2136        public void run() {
2137            mPosted = false;
2138            mPrinterUnavailable = true;
2139            onPrinterUnavailable(mPrinter);
2140        }
2141    }
2142
2143    private static final class PrinterHolder {
2144        PrinterInfo printer;
2145        boolean removed;
2146
2147        public PrinterHolder(PrinterInfo printer) {
2148            this.printer = printer;
2149        }
2150    }
2151
2152
2153    /**
2154     * Check if the user has ever printed a document
2155     *
2156     * @return true iff the user has ever printed a document
2157     */
2158    private boolean hasUserEverPrinted() {
2159        SharedPreferences preferences = getSharedPreferences(HAS_PRINTED_PREF, MODE_PRIVATE);
2160
2161        return preferences.getBoolean(HAS_PRINTED_PREF, false);
2162    }
2163
2164    /**
2165     * Remember that the user printed a document
2166     */
2167    private void setUserPrinted() {
2168        SharedPreferences preferences = getSharedPreferences(HAS_PRINTED_PREF, MODE_PRIVATE);
2169
2170        if (!preferences.getBoolean(HAS_PRINTED_PREF, false)) {
2171            SharedPreferences.Editor edit = preferences.edit();
2172
2173            edit.putBoolean(HAS_PRINTED_PREF, true);
2174            edit.apply();
2175        }
2176    }
2177
2178    private final class DestinationAdapter extends BaseAdapter
2179            implements PrinterRegistry.OnPrintersChangeListener {
2180        private final List<PrinterHolder> mPrinterHolders = new ArrayList<>();
2181
2182        private final PrinterHolder mFakePdfPrinterHolder;
2183
2184        private boolean mHistoricalPrintersLoaded;
2185
2186        /**
2187         * Has the {@link #mDestinationSpinner} ever used a view from printer_dropdown_prompt
2188         */
2189        private boolean hadPromptView;
2190
2191        public DestinationAdapter() {
2192            mHistoricalPrintersLoaded = mPrinterRegistry.areHistoricalPrintersLoaded();
2193            if (mHistoricalPrintersLoaded) {
2194                addPrinters(mPrinterHolders, mPrinterRegistry.getPrinters());
2195            }
2196            mPrinterRegistry.setOnPrintersChangeListener(this);
2197            mFakePdfPrinterHolder = new PrinterHolder(createFakePdfPrinter());
2198        }
2199
2200        public PrinterInfo getPdfPrinter() {
2201            return mFakePdfPrinterHolder.printer;
2202        }
2203
2204        public int getPrinterIndex(PrinterId printerId) {
2205            for (int i = 0; i < getCount(); i++) {
2206                PrinterHolder printerHolder = (PrinterHolder) getItem(i);
2207                if (printerHolder != null && !printerHolder.removed
2208                        && printerHolder.printer.getId().equals(printerId)) {
2209                    return i;
2210                }
2211            }
2212            return AdapterView.INVALID_POSITION;
2213        }
2214
2215        public void ensurePrinterInVisibleAdapterPosition(PrinterInfo printer) {
2216            final int printerCount = mPrinterHolders.size();
2217            boolean isKnownPrinter = false;
2218            for (int i = 0; i < printerCount; i++) {
2219                PrinterHolder printerHolder = mPrinterHolders.get(i);
2220
2221                if (printerHolder.printer.getId().equals(printer.getId())) {
2222                    isKnownPrinter = true;
2223
2224                    // If already in the list - do nothing.
2225                    if (i < getCount() - 2) {
2226                        break;
2227                    }
2228                    // Else replace the last one (two items are not printers).
2229                    final int lastPrinterIndex = getCount() - 3;
2230                    mPrinterHolders.set(i, mPrinterHolders.get(lastPrinterIndex));
2231                    mPrinterHolders.set(lastPrinterIndex, printerHolder);
2232                    break;
2233                }
2234            }
2235
2236            if (!isKnownPrinter) {
2237                PrinterHolder printerHolder = new PrinterHolder(printer);
2238                printerHolder.removed = true;
2239
2240                mPrinterHolders.add(Math.max(0, getCount() - 3), printerHolder);
2241            }
2242
2243            // Force reload to adjust selection in PrintersObserver.onChanged()
2244            notifyDataSetChanged();
2245        }
2246
2247        @Override
2248        public int getCount() {
2249            if (mHistoricalPrintersLoaded) {
2250                return Math.min(mPrinterHolders.size() + 2, DEST_ADAPTER_MAX_ITEM_COUNT);
2251            }
2252            return 0;
2253        }
2254
2255        @Override
2256        public boolean isEnabled(int position) {
2257            Object item = getItem(position);
2258            if (item instanceof PrinterHolder) {
2259                PrinterHolder printerHolder = (PrinterHolder) item;
2260                return !printerHolder.removed
2261                        && printerHolder.printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
2262            }
2263            return true;
2264        }
2265
2266        @Override
2267        public Object getItem(int position) {
2268            if (mPrinterHolders.isEmpty()) {
2269                if (position == 0) {
2270                    return mFakePdfPrinterHolder;
2271                }
2272            } else {
2273                if (position < 1) {
2274                    return mPrinterHolders.get(position);
2275                }
2276                if (position == 1) {
2277                    return mFakePdfPrinterHolder;
2278                }
2279                if (position < getCount() - 1) {
2280                    return mPrinterHolders.get(position - 1);
2281                }
2282            }
2283            return null;
2284        }
2285
2286        @Override
2287        public long getItemId(int position) {
2288            if (mPrinterHolders.isEmpty()) {
2289                if (position == 0) {
2290                    return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
2291                } else if (position == 1) {
2292                    return DEST_ADAPTER_ITEM_ID_MORE;
2293                }
2294            } else {
2295                if (position == 1) {
2296                    return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
2297                }
2298                if (position == getCount() - 1) {
2299                    return DEST_ADAPTER_ITEM_ID_MORE;
2300                }
2301            }
2302            return position;
2303        }
2304
2305        @Override
2306        public View getDropDownView(int position, View convertView, ViewGroup parent) {
2307            View view = getView(position, convertView, parent);
2308            view.setEnabled(isEnabled(position));
2309            return view;
2310        }
2311
2312        private String getMoreItemTitle() {
2313            if (mArePrintServicesEnabled) {
2314                return getString(R.string.all_printers);
2315            } else {
2316                return getString(R.string.print_add_printer);
2317            }
2318        }
2319
2320        @Override
2321        public View getView(int position, View convertView, ViewGroup parent) {
2322            if (mShowDestinationPrompt) {
2323                if (convertView == null) {
2324                    convertView = getLayoutInflater().inflate(
2325                            R.layout.printer_dropdown_prompt, parent, false);
2326                    hadPromptView = true;
2327                }
2328
2329                return convertView;
2330            } else {
2331                // We don't know if we got an recyled printer_dropdown_prompt, hence do not use it
2332                if (hadPromptView || convertView == null) {
2333                    convertView = getLayoutInflater().inflate(
2334                            R.layout.printer_dropdown_item, parent, false);
2335                }
2336            }
2337
2338            CharSequence title = null;
2339            CharSequence subtitle = null;
2340            Drawable icon = null;
2341
2342            if (mPrinterHolders.isEmpty()) {
2343                if (position == 0 && getPdfPrinter() != null) {
2344                    PrinterHolder printerHolder = (PrinterHolder) getItem(position);
2345                    title = printerHolder.printer.getName();
2346                    icon = getResources().getDrawable(R.drawable.ic_menu_savetopdf, null);
2347                } else if (position == 1) {
2348                    title = getMoreItemTitle();
2349                }
2350            } else {
2351                if (position == 1 && getPdfPrinter() != null) {
2352                    PrinterHolder printerHolder = (PrinterHolder) getItem(position);
2353                    title = printerHolder.printer.getName();
2354                    icon = getResources().getDrawable(R.drawable.ic_menu_savetopdf, null);
2355                } else if (position == getCount() - 1) {
2356                    title = getMoreItemTitle();
2357                } else {
2358                    PrinterHolder printerHolder = (PrinterHolder) getItem(position);
2359                    PrinterInfo printInfo = printerHolder.printer;
2360
2361                    title = printInfo.getName();
2362                    icon = printInfo.loadIcon(PrintActivity.this);
2363                    subtitle = printInfo.getDescription();
2364                }
2365            }
2366
2367            TextView titleView = (TextView) convertView.findViewById(R.id.title);
2368            titleView.setText(title);
2369
2370            TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle);
2371            if (!TextUtils.isEmpty(subtitle)) {
2372                subtitleView.setText(subtitle);
2373                subtitleView.setVisibility(View.VISIBLE);
2374            } else {
2375                subtitleView.setText(null);
2376                subtitleView.setVisibility(View.GONE);
2377            }
2378
2379            ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
2380            if (icon != null) {
2381                iconView.setVisibility(View.VISIBLE);
2382                if (!isEnabled(position)) {
2383                    icon.mutate();
2384
2385                    TypedValue value = new TypedValue();
2386                    getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true);
2387                    icon.setAlpha((int)(value.getFloat() * 255));
2388                }
2389                iconView.setImageDrawable(icon);
2390            } else {
2391                iconView.setVisibility(View.INVISIBLE);
2392            }
2393
2394            return convertView;
2395        }
2396
2397        @Override
2398        public void onPrintersChanged(List<PrinterInfo> printers) {
2399            // We rearrange the printers if the user selects a printer
2400            // not shown in the initial short list. Therefore, we have
2401            // to keep the printer order.
2402
2403            // Check if historical printers are loaded as this adapter is open
2404            // for busyness only if they are. This member is updated here and
2405            // when the adapter is created because the historical printers may
2406            // be loaded before or after the adapter is created.
2407            mHistoricalPrintersLoaded = mPrinterRegistry.areHistoricalPrintersLoaded();
2408
2409            // No old printers - do not bother keeping their position.
2410            if (mPrinterHolders.isEmpty()) {
2411                addPrinters(mPrinterHolders, printers);
2412                notifyDataSetChanged();
2413                return;
2414            }
2415
2416            // Add the new printers to a map.
2417            ArrayMap<PrinterId, PrinterInfo> newPrintersMap = new ArrayMap<>();
2418            final int printerCount = printers.size();
2419            for (int i = 0; i < printerCount; i++) {
2420                PrinterInfo printer = printers.get(i);
2421                newPrintersMap.put(printer.getId(), printer);
2422            }
2423
2424            List<PrinterHolder> newPrinterHolders = new ArrayList<>();
2425
2426            // Update printers we already have which are either updated or removed.
2427            // We do not remove the currently selected printer.
2428            final int oldPrinterCount = mPrinterHolders.size();
2429            for (int i = 0; i < oldPrinterCount; i++) {
2430                PrinterHolder printerHolder = mPrinterHolders.get(i);
2431                PrinterId oldPrinterId = printerHolder.printer.getId();
2432                PrinterInfo updatedPrinter = newPrintersMap.remove(oldPrinterId);
2433                if (updatedPrinter != null) {
2434                    printerHolder.printer = updatedPrinter;
2435                    printerHolder.removed = false;
2436                    newPrinterHolders.add(printerHolder);
2437                } else if (mCurrentPrinter != null && mCurrentPrinter.getId().equals(oldPrinterId)){
2438                    printerHolder.removed = true;
2439                    newPrinterHolders.add(printerHolder);
2440                }
2441            }
2442
2443            // Add the rest of the new printers, i.e. what is left.
2444            addPrinters(newPrinterHolders, newPrintersMap.values());
2445
2446            mPrinterHolders.clear();
2447            mPrinterHolders.addAll(newPrinterHolders);
2448
2449            notifyDataSetChanged();
2450        }
2451
2452        @Override
2453        public void onPrintersInvalid() {
2454            mPrinterHolders.clear();
2455            notifyDataSetInvalidated();
2456        }
2457
2458        public PrinterHolder getPrinterHolder(PrinterId printerId) {
2459            final int itemCount = getCount();
2460            for (int i = 0; i < itemCount; i++) {
2461                Object item = getItem(i);
2462                if (item instanceof PrinterHolder) {
2463                    PrinterHolder printerHolder = (PrinterHolder) item;
2464                    if (printerId.equals(printerHolder.printer.getId())) {
2465                        return printerHolder;
2466                    }
2467                }
2468            }
2469            return null;
2470        }
2471
2472        /**
2473         * Remove a printer from the holders if it is marked as removed.
2474         *
2475         * @param printerId the id of the printer to remove.
2476         *
2477         * @return true iff the printer was removed.
2478         */
2479        public boolean pruneRemovedPrinter(PrinterId printerId) {
2480            final int holderCounts = mPrinterHolders.size();
2481            for (int i = holderCounts - 1; i >= 0; i--) {
2482                PrinterHolder printerHolder = mPrinterHolders.get(i);
2483
2484                if (printerHolder.printer.getId().equals(printerId) && printerHolder.removed) {
2485                    mPrinterHolders.remove(i);
2486                    return true;
2487                }
2488            }
2489
2490            return false;
2491        }
2492
2493        private void addPrinters(List<PrinterHolder> list, Collection<PrinterInfo> printers) {
2494            for (PrinterInfo printer : printers) {
2495                PrinterHolder printerHolder = new PrinterHolder(printer);
2496                list.add(printerHolder);
2497            }
2498        }
2499
2500        private PrinterInfo createFakePdfPrinter() {
2501            ArraySet<MediaSize> allMediaSizes = MediaSize.getAllPredefinedSizes();
2502            MediaSize defaultMediaSize = MediaSizeUtils.getDefault(PrintActivity.this);
2503
2504            PrinterId printerId = new PrinterId(getComponentName(), "PDF printer");
2505
2506            PrinterCapabilitiesInfo.Builder builder =
2507                    new PrinterCapabilitiesInfo.Builder(printerId);
2508
2509            final int mediaSizeCount = allMediaSizes.size();
2510            for (int i = 0; i < mediaSizeCount; i++) {
2511                MediaSize mediaSize = allMediaSizes.valueAt(i);
2512                builder.addMediaSize(mediaSize, mediaSize.equals(defaultMediaSize));
2513            }
2514
2515            builder.addResolution(new Resolution("PDF resolution", "PDF resolution", 300, 300),
2516                    true);
2517            builder.setColorModes(PrintAttributes.COLOR_MODE_COLOR
2518                    | PrintAttributes.COLOR_MODE_MONOCHROME, PrintAttributes.COLOR_MODE_COLOR);
2519
2520            return new PrinterInfo.Builder(printerId, getString(R.string.save_as_pdf),
2521                    PrinterInfo.STATUS_IDLE).setCapabilities(builder.build()).build();
2522        }
2523    }
2524
2525    private final class PrintersObserver extends DataSetObserver {
2526        @Override
2527        public void onChanged() {
2528            PrinterInfo oldPrinterState = mCurrentPrinter;
2529            if (oldPrinterState == null) {
2530                return;
2531            }
2532
2533            PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
2534                    oldPrinterState.getId());
2535            PrinterInfo newPrinterState = printerHolder.printer;
2536
2537            if (printerHolder.removed) {
2538                onPrinterUnavailable(newPrinterState);
2539            }
2540
2541            if (mDestinationSpinner.getSelectedItem() != printerHolder) {
2542                mDestinationSpinner.setSelection(
2543                        mDestinationSpinnerAdapter.getPrinterIndex(newPrinterState.getId()));
2544            }
2545
2546            if (oldPrinterState.equals(newPrinterState)) {
2547                return;
2548            }
2549
2550            PrinterCapabilitiesInfo oldCapab = oldPrinterState.getCapabilities();
2551            PrinterCapabilitiesInfo newCapab = newPrinterState.getCapabilities();
2552
2553            final boolean hadCabab = oldCapab != null;
2554            final boolean hasCapab = newCapab != null;
2555            final boolean gotCapab = oldCapab == null && newCapab != null;
2556            final boolean lostCapab = oldCapab != null && newCapab == null;
2557            final boolean capabChanged = capabilitiesChanged(oldCapab, newCapab);
2558
2559            final int oldStatus = oldPrinterState.getStatus();
2560            final int newStatus = newPrinterState.getStatus();
2561
2562            final boolean isActive = newStatus != PrinterInfo.STATUS_UNAVAILABLE;
2563            final boolean becameActive = (oldStatus == PrinterInfo.STATUS_UNAVAILABLE
2564                    && oldStatus != newStatus);
2565            final boolean becameInactive = (newStatus == PrinterInfo.STATUS_UNAVAILABLE
2566                    && oldStatus != newStatus);
2567
2568            mPrinterAvailabilityDetector.updatePrinter(newPrinterState);
2569
2570            mCurrentPrinter = newPrinterState;
2571
2572            final boolean updateNeeded = ((capabChanged && hasCapab && isActive)
2573                    || (becameActive && hasCapab) || (isActive && gotCapab));
2574
2575            if (capabChanged && hasCapab) {
2576                updatePrintAttributesFromCapabilities(newCapab);
2577            }
2578
2579            if (updateNeeded) {
2580                updatePrintPreviewController(false);
2581            }
2582
2583            if ((isActive && gotCapab) || (becameActive && hasCapab)) {
2584                onPrinterAvailable(newPrinterState);
2585            } else if ((becameInactive && hadCabab) || (isActive && lostCapab)) {
2586                onPrinterUnavailable(newPrinterState);
2587            }
2588
2589            if (updateNeeded && canUpdateDocument()) {
2590                updateDocument(false);
2591            }
2592
2593            // Force a reload of the enabled print services to update mAdvancedPrintOptionsActivity
2594            // in onLoadFinished();
2595            getLoaderManager().getLoader(LOADER_ID_ENABLED_PRINT_SERVICES).forceLoad();
2596
2597            updateOptionsUi();
2598            updateSummary();
2599        }
2600
2601        private boolean capabilitiesChanged(PrinterCapabilitiesInfo oldCapabilities,
2602                PrinterCapabilitiesInfo newCapabilities) {
2603            if (oldCapabilities == null) {
2604                if (newCapabilities != null) {
2605                    return true;
2606                }
2607            } else if (!oldCapabilities.equals(newCapabilities)) {
2608                return true;
2609            }
2610            return false;
2611        }
2612    }
2613
2614    private final class MyOnItemSelectedListener implements AdapterView.OnItemSelectedListener {
2615        @Override
2616        public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) {
2617            boolean clearRanges = false;
2618
2619            if (spinner == mDestinationSpinner) {
2620                if (position == AdapterView.INVALID_POSITION) {
2621                    return;
2622                }
2623
2624                if (id == DEST_ADAPTER_ITEM_ID_MORE) {
2625                    startSelectPrinterActivity();
2626                    return;
2627                }
2628
2629                PrinterHolder currentItem = (PrinterHolder) mDestinationSpinner.getSelectedItem();
2630                PrinterInfo currentPrinter = (currentItem != null) ? currentItem.printer : null;
2631
2632                // Why on earth item selected is called if no selection changed.
2633                if (mCurrentPrinter == currentPrinter) {
2634                    return;
2635                }
2636
2637                PrinterId oldId = null;
2638                if (mCurrentPrinter != null) {
2639                    oldId = mCurrentPrinter.getId();
2640                }
2641
2642                mCurrentPrinter = currentPrinter;
2643
2644                if (oldId != null) {
2645                    boolean printerRemoved = mDestinationSpinnerAdapter.pruneRemovedPrinter(oldId);
2646
2647                    if (printerRemoved) {
2648                        // Trigger PrinterObserver.onChanged to adjust selection. This will call
2649                        // this function again.
2650                        mDestinationSpinnerAdapter.notifyDataSetChanged();
2651                        return;
2652                    }
2653                }
2654
2655                PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
2656                        currentPrinter.getId());
2657                if (!printerHolder.removed) {
2658                    setState(STATE_CONFIGURING);
2659                    ensurePreviewUiShown();
2660                }
2661
2662                mPrintJob.setPrinterId(currentPrinter.getId());
2663                mPrintJob.setPrinterName(currentPrinter.getName());
2664
2665                mPrinterRegistry.setTrackedPrinter(currentPrinter.getId());
2666
2667                PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities();
2668                if (capabilities != null) {
2669                    updatePrintAttributesFromCapabilities(capabilities);
2670                }
2671
2672                mPrinterAvailabilityDetector.updatePrinter(currentPrinter);
2673
2674                // Force a reload of the enabled print services to update
2675                // mAdvancedPrintOptionsActivity in onLoadFinished();
2676                getLoaderManager().getLoader(LOADER_ID_ENABLED_PRINT_SERVICES).forceLoad();
2677            } else if (spinner == mMediaSizeSpinner) {
2678                SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position);
2679                PrintAttributes attributes = mPrintJob.getAttributes();
2680
2681                MediaSize newMediaSize;
2682                if (mOrientationSpinner.getSelectedItemPosition() == 0) {
2683                    newMediaSize = mediaItem.value.asPortrait();
2684                } else {
2685                    newMediaSize = mediaItem.value.asLandscape();
2686                }
2687
2688                if (newMediaSize != attributes.getMediaSize()) {
2689                    clearRanges = true;
2690                    attributes.setMediaSize(newMediaSize);
2691                }
2692            } else if (spinner == mColorModeSpinner) {
2693                SpinnerItem<Integer> colorModeItem = mColorModeSpinnerAdapter.getItem(position);
2694                mPrintJob.getAttributes().setColorMode(colorModeItem.value);
2695            } else if (spinner == mDuplexModeSpinner) {
2696                SpinnerItem<Integer> duplexModeItem = mDuplexModeSpinnerAdapter.getItem(position);
2697                mPrintJob.getAttributes().setDuplexMode(duplexModeItem.value);
2698            } else if (spinner == mOrientationSpinner) {
2699                SpinnerItem<Integer> orientationItem = mOrientationSpinnerAdapter.getItem(position);
2700                PrintAttributes attributes = mPrintJob.getAttributes();
2701                if (mMediaSizeSpinner.getSelectedItem() != null) {
2702                    boolean isPortrait = attributes.isPortrait();
2703
2704                    if (isPortrait != (orientationItem.value == ORIENTATION_PORTRAIT)) {
2705                        clearRanges = true;
2706                        if (orientationItem.value == ORIENTATION_PORTRAIT) {
2707                            attributes.copyFrom(attributes.asPortrait());
2708                        } else {
2709                            attributes.copyFrom(attributes.asLandscape());
2710                        }
2711                    }
2712                }
2713            } else if (spinner == mRangeOptionsSpinner) {
2714                if (mRangeOptionsSpinner.getSelectedItemPosition() == 0) {
2715                    clearRanges = true;
2716                    mPageRangeEditText.setText("");
2717                } else if (TextUtils.isEmpty(mPageRangeEditText.getText())) {
2718                    mPageRangeEditText.setError("");
2719                }
2720            }
2721
2722            if (clearRanges) {
2723                clearPageRanges();
2724            }
2725
2726            updateOptionsUi();
2727
2728            if (canUpdateDocument()) {
2729                updateDocument(false);
2730            }
2731        }
2732
2733        @Override
2734        public void onNothingSelected(AdapterView<?> parent) {
2735            /* do nothing*/
2736        }
2737    }
2738
2739    private final class SelectAllOnFocusListener implements OnFocusChangeListener {
2740        @Override
2741        public void onFocusChange(View view, boolean hasFocus) {
2742            EditText editText = (EditText) view;
2743            if (!TextUtils.isEmpty(editText.getText())) {
2744                editText.setSelection(editText.getText().length());
2745            }
2746
2747            if (view == mPageRangeEditText && !hasFocus && mPageRangeEditText.getError() == null) {
2748                updateSelectedPagesFromTextField();
2749            }
2750        }
2751    }
2752
2753    private final class RangeTextWatcher implements TextWatcher {
2754        @Override
2755        public void onTextChanged(CharSequence s, int start, int before, int count) {
2756            /* do nothing */
2757        }
2758
2759        @Override
2760        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2761            /* do nothing */
2762        }
2763
2764        @Override
2765        public void afterTextChanged(Editable editable) {
2766            final boolean hadErrors = hasErrors();
2767
2768            PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
2769            final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
2770            PageRange[] ranges = PageRangeUtils.parsePageRanges(editable, pageCount);
2771
2772            if (ranges.length == 0) {
2773                if (mPageRangeEditText.getError() == null) {
2774                    mPageRangeEditText.setError("");
2775                    updateOptionsUi();
2776                }
2777                return;
2778            }
2779
2780            if (mPageRangeEditText.getError() != null) {
2781                mPageRangeEditText.setError(null);
2782                updateOptionsUi();
2783            }
2784
2785            if (hadErrors && canUpdateDocument()) {
2786                updateDocument(false);
2787            }
2788        }
2789    }
2790
2791    private final class EditTextWatcher implements TextWatcher {
2792        @Override
2793        public void onTextChanged(CharSequence s, int start, int before, int count) {
2794            /* do nothing */
2795        }
2796
2797        @Override
2798        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2799            /* do nothing */
2800        }
2801
2802        @Override
2803        public void afterTextChanged(Editable editable) {
2804            final boolean hadErrors = hasErrors();
2805
2806            if (editable.length() == 0) {
2807                if (mCopiesEditText.getError() == null) {
2808                    mCopiesEditText.setError("");
2809                    updateOptionsUi();
2810                }
2811                return;
2812            }
2813
2814            int copies = 0;
2815            try {
2816                copies = Integer.parseInt(editable.toString());
2817            } catch (NumberFormatException nfe) {
2818                /* ignore */
2819            }
2820
2821            if (copies < MIN_COPIES) {
2822                if (mCopiesEditText.getError() == null) {
2823                    mCopiesEditText.setError("");
2824                    updateOptionsUi();
2825                }
2826                return;
2827            }
2828
2829            mPrintJob.setCopies(copies);
2830
2831            if (mCopiesEditText.getError() != null) {
2832                mCopiesEditText.setError(null);
2833                updateOptionsUi();
2834            }
2835
2836            if (hadErrors && canUpdateDocument()) {
2837                updateDocument(false);
2838            }
2839        }
2840    }
2841
2842    private final class ProgressMessageController implements Runnable {
2843        private static final long PROGRESS_TIMEOUT_MILLIS = 1000;
2844
2845        private final Handler mHandler;
2846
2847        private boolean mPosted;
2848
2849        /** State before run was executed */
2850        private int mPreviousState = -1;
2851
2852        public ProgressMessageController(Context context) {
2853            mHandler = new Handler(context.getMainLooper(), null, false);
2854        }
2855
2856        public void post() {
2857            if (mState == STATE_UPDATE_SLOW) {
2858                setState(STATE_UPDATE_SLOW);
2859                ensureProgressUiShown();
2860                updateOptionsUi();
2861
2862                return;
2863            } else if (mPosted) {
2864                return;
2865            }
2866            mPreviousState = -1;
2867            mPosted = true;
2868            mHandler.postDelayed(this, PROGRESS_TIMEOUT_MILLIS);
2869        }
2870
2871        private int getStateAfterCancel() {
2872            if (mPreviousState == -1) {
2873                return mState;
2874            } else {
2875                return mPreviousState;
2876            }
2877        }
2878
2879        public int cancel() {
2880            if (!mPosted) {
2881                return getStateAfterCancel();
2882            }
2883            mPosted = false;
2884            mHandler.removeCallbacks(this);
2885
2886            return getStateAfterCancel();
2887        }
2888
2889        @Override
2890        public void run() {
2891            mPosted = false;
2892            mPreviousState = mState;
2893            setState(STATE_UPDATE_SLOW);
2894            ensureProgressUiShown();
2895            updateOptionsUi();
2896        }
2897    }
2898
2899    private static final class DocumentTransformer implements ServiceConnection {
2900        private static final String TEMP_FILE_PREFIX = "print_job";
2901        private static final String TEMP_FILE_EXTENSION = ".pdf";
2902
2903        private final Context mContext;
2904
2905        private final MutexFileProvider mFileProvider;
2906
2907        private final PrintJobInfo mPrintJob;
2908
2909        private final PageRange[] mPagesToShred;
2910
2911        private final PrintAttributes mAttributesToApply;
2912
2913        private final Runnable mCallback;
2914
2915        public DocumentTransformer(Context context, PrintJobInfo printJob,
2916                MutexFileProvider fileProvider, PrintAttributes attributes,
2917                Runnable callback) {
2918            mContext = context;
2919            mPrintJob = printJob;
2920            mFileProvider = fileProvider;
2921            mCallback = callback;
2922            mPagesToShred = computePagesToShred(mPrintJob);
2923            mAttributesToApply = attributes;
2924        }
2925
2926        public void transform() {
2927            // If we have only the pages we want, done.
2928            if (mPagesToShred.length <= 0 && mAttributesToApply == null) {
2929                mCallback.run();
2930                return;
2931            }
2932
2933            // Bind to the manipulation service and the work
2934            // will be performed upon connection to the service.
2935            Intent intent = new Intent(PdfManipulationService.ACTION_GET_EDITOR);
2936            intent.setClass(mContext, PdfManipulationService.class);
2937            mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
2938        }
2939
2940        @Override
2941        public void onServiceConnected(ComponentName name, IBinder service) {
2942            final IPdfEditor editor = IPdfEditor.Stub.asInterface(service);
2943            new AsyncTask<Void, Void, Void>() {
2944                @Override
2945                protected Void doInBackground(Void... params) {
2946                    // It's OK to access the data members as they are
2947                    // final and this code is the last one to touch
2948                    // them as shredding is the very last step, so the
2949                    // UI is not interactive at this point.
2950                    doTransform(editor);
2951                    updatePrintJob();
2952                    return null;
2953                }
2954
2955                @Override
2956                protected void onPostExecute(Void aVoid) {
2957                    mContext.unbindService(DocumentTransformer.this);
2958                    mCallback.run();
2959                }
2960            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
2961        }
2962
2963        @Override
2964        public void onServiceDisconnected(ComponentName name) {
2965            /* do nothing */
2966        }
2967
2968        private void doTransform(IPdfEditor editor) {
2969            File tempFile = null;
2970            ParcelFileDescriptor src = null;
2971            ParcelFileDescriptor dst = null;
2972            InputStream in = null;
2973            OutputStream out = null;
2974            try {
2975                File jobFile = mFileProvider.acquireFile(null);
2976                src = ParcelFileDescriptor.open(jobFile, ParcelFileDescriptor.MODE_READ_WRITE);
2977
2978                // Open the document.
2979                editor.openDocument(src);
2980
2981                // We passed the fd over IPC, close this one.
2982                src.close();
2983
2984                // Drop the pages.
2985                editor.removePages(mPagesToShred);
2986
2987                // Apply print attributes if needed.
2988                if (mAttributesToApply != null) {
2989                    editor.applyPrintAttributes(mAttributesToApply);
2990                }
2991
2992                // Write the modified PDF to a temp file.
2993                tempFile = File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_EXTENSION,
2994                        mContext.getCacheDir());
2995                dst = ParcelFileDescriptor.open(tempFile, ParcelFileDescriptor.MODE_READ_WRITE);
2996                editor.write(dst);
2997                dst.close();
2998
2999                // Close the document.
3000                editor.closeDocument();
3001
3002                // Copy the temp file over the print job file.
3003                jobFile.delete();
3004                in = new FileInputStream(tempFile);
3005                out = new FileOutputStream(jobFile);
3006                Streams.copy(in, out);
3007            } catch (IOException|RemoteException e) {
3008                Log.e(LOG_TAG, "Error dropping pages", e);
3009            } finally {
3010                IoUtils.closeQuietly(src);
3011                IoUtils.closeQuietly(dst);
3012                IoUtils.closeQuietly(in);
3013                IoUtils.closeQuietly(out);
3014                if (tempFile != null) {
3015                    tempFile.delete();
3016                }
3017                mFileProvider.releaseFile();
3018            }
3019        }
3020
3021        private void updatePrintJob() {
3022            // Update the print job pages.
3023            final int newPageCount = PageRangeUtils.getNormalizedPageCount(
3024                    mPrintJob.getPages(), 0);
3025            mPrintJob.setPages(new PageRange[]{PageRange.ALL_PAGES});
3026
3027            // Update the print job document info.
3028            PrintDocumentInfo oldDocInfo = mPrintJob.getDocumentInfo();
3029            PrintDocumentInfo newDocInfo = new PrintDocumentInfo
3030                    .Builder(oldDocInfo.getName())
3031                    .setContentType(oldDocInfo.getContentType())
3032                    .setPageCount(newPageCount)
3033                    .build();
3034            mPrintJob.setDocumentInfo(newDocInfo);
3035        }
3036
3037        private static PageRange[] computePagesToShred(PrintJobInfo printJob) {
3038            List<PageRange> rangesToShred = new ArrayList<>();
3039            PageRange previousRange = null;
3040
3041            PageRange[] printedPages = printJob.getPages();
3042            final int rangeCount = printedPages.length;
3043            for (int i = 0; i < rangeCount; i++) {
3044                PageRange range = printedPages[i];
3045
3046                if (previousRange == null) {
3047                    final int startPageIdx = 0;
3048                    final int endPageIdx = range.getStart() - 1;
3049                    if (startPageIdx <= endPageIdx) {
3050                        PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
3051                        rangesToShred.add(removedRange);
3052                    }
3053                } else {
3054                    final int startPageIdx = previousRange.getEnd() + 1;
3055                    final int endPageIdx = range.getStart() - 1;
3056                    if (startPageIdx <= endPageIdx) {
3057                        PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
3058                        rangesToShred.add(removedRange);
3059                    }
3060                }
3061
3062                if (i == rangeCount - 1) {
3063                    if (range.getEnd() != Integer.MAX_VALUE) {
3064                        rangesToShred.add(new PageRange(range.getEnd() + 1, Integer.MAX_VALUE));
3065                    }
3066                }
3067
3068                previousRange = range;
3069            }
3070
3071            PageRange[] result = new PageRange[rangesToShred.size()];
3072            rangesToShred.toArray(result);
3073            return result;
3074        }
3075    }
3076}
3077