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