PrintActivity.java revision 3ef8e203c887a0cf6c121fdbb428e2fcef21758a
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.Fragment;
21import android.app.FragmentTransaction;
22import android.content.ActivityNotFoundException;
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.ServiceConnection;
27import android.content.pm.PackageInfo;
28import android.content.pm.PackageManager.NameNotFoundException;
29import android.content.pm.ResolveInfo;
30import android.content.res.Configuration;
31import android.database.DataSetObserver;
32import android.graphics.drawable.Drawable;
33import android.net.Uri;
34import android.os.AsyncTask;
35import android.os.Bundle;
36import android.os.Handler;
37import android.os.IBinder;
38import android.os.ParcelFileDescriptor;
39import android.os.RemoteException;
40import android.print.IPrintDocumentAdapter;
41import android.print.PageRange;
42import android.print.PrintAttributes;
43import android.print.PrintAttributes.MediaSize;
44import android.print.PrintAttributes.Resolution;
45import android.print.PrintDocumentInfo;
46import android.print.PrintJobInfo;
47import android.print.PrintManager;
48import android.print.PrinterCapabilitiesInfo;
49import android.print.PrinterId;
50import android.print.PrinterInfo;
51import android.printservice.PrintService;
52import android.provider.DocumentsContract;
53import android.text.Editable;
54import android.text.TextUtils;
55import android.text.TextUtils.SimpleStringSplitter;
56import android.text.TextWatcher;
57import android.util.ArrayMap;
58import android.util.Log;
59import android.view.KeyEvent;
60import android.view.View;
61import android.view.View.OnClickListener;
62import android.view.View.OnFocusChangeListener;
63import android.view.ViewGroup;
64import android.view.inputmethod.InputMethodManager;
65import android.widget.AdapterView;
66import android.widget.AdapterView.OnItemSelectedListener;
67import android.widget.ArrayAdapter;
68import android.widget.BaseAdapter;
69import android.widget.Button;
70import android.widget.EditText;
71import android.widget.ImageView;
72import android.widget.Spinner;
73import android.widget.TextView;
74
75import com.android.printspooler.R;
76import com.android.printspooler.model.MutexFileProvider;
77import com.android.printspooler.model.PrintSpoolerProvider;
78import com.android.printspooler.model.PrintSpoolerService;
79import com.android.printspooler.model.RemotePrintDocument;
80import com.android.printspooler.model.RemotePrintDocument.RemotePrintDocumentInfo;
81import com.android.printspooler.renderer.IPdfEditor;
82import com.android.printspooler.renderer.PdfManipulationService;
83import com.android.printspooler.util.MediaSizeUtils;
84import com.android.printspooler.util.MediaSizeUtils.MediaSizeComparator;
85import com.android.printspooler.util.PageRangeUtils;
86import com.android.printspooler.util.PrintOptionUtils;
87import com.android.printspooler.widget.PrintContentView;
88import com.android.printspooler.widget.PrintContentView.OptionsStateChangeListener;
89import com.android.printspooler.widget.PrintContentView.OptionsStateController;
90import libcore.io.IoUtils;
91import libcore.io.Streams;
92
93import java.io.File;
94import java.io.FileInputStream;
95import java.io.FileOutputStream;
96import java.io.IOException;
97import java.io.InputStream;
98import java.io.OutputStream;
99import java.util.ArrayList;
100import java.util.Arrays;
101import java.util.Collection;
102import java.util.Collections;
103import java.util.List;
104import java.util.regex.Matcher;
105import java.util.regex.Pattern;
106
107public class PrintActivity extends Activity implements RemotePrintDocument.UpdateResultCallbacks,
108        PrintErrorFragment.OnActionListener, PageAdapter.ContentCallbacks,
109        OptionsStateChangeListener, OptionsStateController {
110    private static final String LOG_TAG = "PrintActivity";
111
112    private static final boolean DEBUG = false;
113
114    public static final String INTENT_EXTRA_PRINTER_ID = "INTENT_EXTRA_PRINTER_ID";
115
116    private static final String FRAGMENT_TAG = "FRAGMENT_TAG";
117
118    private static final int ORIENTATION_PORTRAIT = 0;
119    private static final int ORIENTATION_LANDSCAPE = 1;
120
121    private static final int ACTIVITY_REQUEST_CREATE_FILE = 1;
122    private static final int ACTIVITY_REQUEST_SELECT_PRINTER = 2;
123    private static final int ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS = 3;
124
125    private static final int DEST_ADAPTER_MAX_ITEM_COUNT = 9;
126
127    private static final int DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF = Integer.MAX_VALUE;
128    private static final int DEST_ADAPTER_ITEM_ID_ALL_PRINTERS = Integer.MAX_VALUE - 1;
129
130    private static final int STATE_INITIALIZING = 0;
131    private static final int STATE_CONFIGURING = 1;
132    private static final int STATE_PRINT_CONFIRMED = 2;
133    private static final int STATE_PRINT_CANCELED = 3;
134    private static final int STATE_UPDATE_FAILED = 4;
135    private static final int STATE_CREATE_FILE_FAILED = 5;
136    private static final int STATE_PRINTER_UNAVAILABLE = 6;
137    private static final int STATE_UPDATE_SLOW = 7;
138    private static final int STATE_PRINT_COMPLETED = 8;
139
140    private static final int UI_STATE_PREVIEW = 0;
141    private static final int UI_STATE_ERROR = 1;
142    private static final int UI_STATE_PROGRESS = 2;
143
144    private static final int MIN_COPIES = 1;
145    private static final String MIN_COPIES_STRING = String.valueOf(MIN_COPIES);
146
147    private static final Pattern PATTERN_DIGITS = Pattern.compile("[\\d]+");
148
149    private static final Pattern PATTERN_ESCAPE_SPECIAL_CHARS = Pattern.compile(
150            "(?=[]\\[+&|!(){}^\"~*?:\\\\])");
151
152    private static final Pattern PATTERN_PAGE_RANGE = Pattern.compile(
153            "[\\s]*[0-9]+[\\-]?[\\s]*[0-9]*[\\s]*?(([,])"
154                    + "[\\s]*[0-9]+[\\s]*[\\-]?[\\s]*[0-9]*[\\s]*|[\\s]*)+");
155
156    public static final PageRange[] ALL_PAGES_ARRAY = new PageRange[]{PageRange.ALL_PAGES};
157
158    private final PrinterAvailabilityDetector mPrinterAvailabilityDetector =
159            new PrinterAvailabilityDetector();
160
161    private final SimpleStringSplitter mStringCommaSplitter = new SimpleStringSplitter(',');
162
163    private final OnFocusChangeListener mSelectAllOnFocusListener = new SelectAllOnFocusListener();
164
165    private PrintSpoolerProvider mSpoolerProvider;
166
167    private PrintPreviewController mPrintPreviewController;
168
169    private PrintJobInfo mPrintJob;
170    private RemotePrintDocument mPrintedDocument;
171    private PrinterRegistry mPrinterRegistry;
172
173    private EditText mCopiesEditText;
174
175    private TextView mPageRangeTitle;
176    private EditText mPageRangeEditText;
177
178    private Spinner mDestinationSpinner;
179    private DestinationAdapter mDestinationSpinnerAdapter;
180
181    private Spinner mMediaSizeSpinner;
182    private ArrayAdapter<SpinnerItem<MediaSize>> mMediaSizeSpinnerAdapter;
183
184    private Spinner mColorModeSpinner;
185    private ArrayAdapter<SpinnerItem<Integer>> mColorModeSpinnerAdapter;
186
187    private Spinner mOrientationSpinner;
188    private ArrayAdapter<SpinnerItem<Integer>> mOrientationSpinnerAdapter;
189
190    private Spinner mRangeOptionsSpinner;
191
192    private PrintContentView mOptionsContent;
193
194    private TextView mSummaryCopies;
195    private TextView mSummaryPaperSize;
196
197    private Button mMoreOptionsButton;
198
199    private ImageView mPrintButton;
200
201    private ProgressMessageController mProgressMessageController;
202    private MutexFileProvider mFileProvider;
203
204    private MediaSizeComparator mMediaSizeComparator;
205
206    private PrinterInfo mCurrentPrinter;
207
208    private PageRange[] mSelectedPages;
209
210    private String mCallingPackageName;
211
212    private int mCurrentPageCount;
213
214    private int mState = STATE_INITIALIZING;
215
216    private int mUiState = UI_STATE_PREVIEW;
217
218    @Override
219    public void onCreate(Bundle savedInstanceState) {
220        super.onCreate(savedInstanceState);
221
222        Bundle extras = getIntent().getExtras();
223
224        mPrintJob = extras.getParcelable(PrintManager.EXTRA_PRINT_JOB);
225        if (mPrintJob == null) {
226            throw new IllegalArgumentException(PrintManager.EXTRA_PRINT_JOB
227                    + " cannot be null");
228        }
229        mPrintJob.setAttributes(new PrintAttributes.Builder().build());
230
231        final IBinder adapter = extras.getBinder(PrintManager.EXTRA_PRINT_DOCUMENT_ADAPTER);
232        if (adapter == null) {
233            throw new IllegalArgumentException(PrintManager.EXTRA_PRINT_DOCUMENT_ADAPTER
234                    + " cannot be null");
235        }
236
237        mCallingPackageName = extras.getString(DocumentsContract.EXTRA_PACKAGE_NAME);
238
239        // This will take just a few milliseconds, so just wait to
240        // bind to the local service before showing the UI.
241        mSpoolerProvider = new PrintSpoolerProvider(this,
242                new Runnable() {
243            @Override
244            public void run() {
245                onConnectedToPrintSpooler(adapter);
246            }
247        });
248    }
249
250    private void onConnectedToPrintSpooler(final IBinder documentAdapter) {
251        // Now that we are bound to the print spooler service,
252        // create the printer registry and wait for it to get
253        // the first batch of results which will be delivered
254        // after reading historical data. This should be pretty
255        // fast, so just wait before showing the UI.
256        mPrinterRegistry = new PrinterRegistry(PrintActivity.this,
257                new Runnable() {
258            @Override
259            public void run() {
260                onPrinterRegistryReady(documentAdapter);
261            }
262        });
263    }
264
265    private void onPrinterRegistryReady(IBinder documentAdapter) {
266        // Now that we are bound to the local print spooler service
267        // and the printer registry loaded the historical printers
268        // we can show the UI without flickering.
269        setTitle(R.string.print_dialog);
270        setContentView(R.layout.print_activity);
271
272        try {
273            mFileProvider = new MutexFileProvider(
274                    PrintSpoolerService.generateFileForPrintJob(
275                            PrintActivity.this, mPrintJob.getId()));
276        } catch (IOException ioe) {
277            // At this point we cannot recover, so just take it down.
278            throw new IllegalStateException("Cannot create print job file", ioe);
279        }
280
281        mPrintPreviewController = new PrintPreviewController(PrintActivity.this,
282                mFileProvider);
283        mPrintedDocument = new RemotePrintDocument(PrintActivity.this,
284                IPrintDocumentAdapter.Stub.asInterface(documentAdapter),
285                mFileProvider, new RemotePrintDocument.DocumentObserver() {
286            @Override
287            public void onDestroy() {
288                setState(STATE_PRINT_CANCELED);
289                finish();
290            }
291        }, PrintActivity.this);
292        mProgressMessageController = new ProgressMessageController(
293                PrintActivity.this);
294        mMediaSizeComparator = new MediaSizeComparator(PrintActivity.this);
295        mDestinationSpinnerAdapter = new DestinationAdapter();
296
297        bindUi();
298        updateOptionsUi();
299
300        // Now show the updated UI to avoid flicker.
301        mOptionsContent.setVisibility(View.VISIBLE);
302        mSelectedPages = computeSelectedPages();
303        mPrintedDocument.start();
304
305        ensurePreviewUiShown();
306
307        setState(STATE_CONFIGURING);
308    }
309
310    @Override
311    public void onPause() {
312        PrintSpoolerService spooler = mSpoolerProvider.getSpooler();
313
314        if (mState == STATE_INITIALIZING) {
315            if (isFinishing()) {
316                spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_CANCELED, null);
317            }
318            super.onPause();
319            return;
320        }
321
322        if (isFinishing()) {
323            spooler.updatePrintJobUserConfigurableOptionsNoPersistence(mPrintJob);
324
325            switch (mState) {
326                case STATE_PRINT_CONFIRMED: {
327                    spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_QUEUED, null);
328                } break;
329
330                case STATE_PRINT_COMPLETED: {
331                    spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_COMPLETED, null);
332                } break;
333
334                case STATE_CREATE_FILE_FAILED: {
335                    spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_FAILED,
336                            getString(R.string.print_write_error_message));
337                } break;
338
339                default: {
340                    spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_CANCELED, null);
341                } break;
342            }
343
344            mProgressMessageController.cancel();
345            mPrinterRegistry.setTrackedPrinter(null);
346            mPrintPreviewController.destroy();
347            mSpoolerProvider.destroy();
348
349            if (mPrintedDocument.isUpdating()) {
350                mPrintedDocument.cancel();
351            }
352            mPrintedDocument.finish();
353            mPrintedDocument.destroy();
354        }
355
356        mPrinterAvailabilityDetector.cancel();
357
358        super.onPause();
359    }
360
361    @Override
362    public boolean onKeyDown(int keyCode, KeyEvent event) {
363        if (keyCode == KeyEvent.KEYCODE_BACK) {
364            event.startTracking();
365            return true;
366        }
367        return super.onKeyDown(keyCode, event);
368    }
369
370    @Override
371    public boolean onKeyUp(int keyCode, KeyEvent event) {
372        if (mState == STATE_INITIALIZING) {
373            finish();
374            return true;
375        }
376
377        if (keyCode == KeyEvent.KEYCODE_BACK
378                && event.isTracking() && !event.isCanceled()) {
379            if (mPrintPreviewController != null && mPrintPreviewController.isOptionsOpened()
380                    && !hasErrors()) {
381                mPrintPreviewController.closeOptions();
382            } else {
383                cancelPrint();
384            }
385            return true;
386        }
387        return super.onKeyUp(keyCode, event);
388    }
389
390    @Override
391    public void onRequestContentUpdate() {
392        if (canUpdateDocument()) {
393            updateDocument(false);
394        }
395    }
396
397    @Override
398    public void onMalformedPdfFile() {
399        mProgressMessageController.cancel();
400        ensureErrorUiShown(null, PrintErrorFragment.ACTION_RETRY);
401
402        setState(STATE_UPDATE_FAILED);
403
404        updateOptionsUi();
405    }
406
407    @Override
408    public void onActionPerformed() {
409        if (mState == STATE_UPDATE_FAILED
410                && canUpdateDocument() && updateDocument(true)) {
411            ensurePreviewUiShown();
412            setState(STATE_CONFIGURING);
413            updateOptionsUi();
414        }
415    }
416
417    public void onUpdateCanceled() {
418        if (DEBUG) {
419            Log.i(LOG_TAG, "onUpdateCanceled()");
420        }
421
422        mProgressMessageController.cancel();
423        ensurePreviewUiShown();
424
425        switch (mState) {
426            case STATE_PRINT_CONFIRMED: {
427                requestCreatePdfFileOrFinish();
428            } break;
429
430            case STATE_PRINT_CANCELED: {
431                finish();
432            } break;
433        }
434    }
435
436    @Override
437    public void onUpdateCompleted(RemotePrintDocumentInfo document) {
438        if (DEBUG) {
439            Log.i(LOG_TAG, "onUpdateCompleted()");
440        }
441
442        mProgressMessageController.cancel();
443        ensurePreviewUiShown();
444
445        // Update the print job with the info for the written document. The page
446        // count we get from the remote document is the pages in the document from
447        // the app perspective but the print job should contain the page count from
448        // print service perspective which is the pages in the written PDF not the
449        // pages in the printed document.
450        PrintDocumentInfo info = document.info;
451        if (info != null) {
452            final int pageCount = PageRangeUtils.getNormalizedPageCount(document.writtenPages,
453                    getAdjustedPageCount(info));
454            PrintDocumentInfo adjustedInfo = new PrintDocumentInfo.Builder(info.getName())
455                    .setContentType(info.getContentType())
456                    .setPageCount(pageCount)
457                    .build();
458            mPrintJob.setDocumentInfo(adjustedInfo);
459            mPrintJob.setPages(document.printedPages);
460        }
461
462        switch (mState) {
463            case STATE_PRINT_CONFIRMED: {
464                requestCreatePdfFileOrFinish();
465            } break;
466
467            case STATE_PRINT_CANCELED: {
468                finish();
469            } break;
470
471            default: {
472                updatePrintPreviewController(document.changed);
473
474                setState(STATE_CONFIGURING);
475                updateOptionsUi();
476            } break;
477        }
478    }
479
480    @Override
481    public void onUpdateFailed(CharSequence error) {
482        if (DEBUG) {
483            Log.i(LOG_TAG, "onUpdateFailed()");
484        }
485
486        mProgressMessageController.cancel();
487        ensureErrorUiShown(error, PrintErrorFragment.ACTION_RETRY);
488
489        setState(STATE_UPDATE_FAILED);
490
491        updateOptionsUi();
492    }
493
494    @Override
495    public void onOptionsOpened() {
496        updateSelectedPagesFromPreview();
497    }
498
499    @Override
500    public void onOptionsClosed() {
501        PageRange[] selectedPages = computeSelectedPages();
502        if (!Arrays.equals(mSelectedPages, selectedPages)) {
503            mSelectedPages = selectedPages;
504
505            // Update preview.
506            updatePrintPreviewController(false);
507        }
508
509        // Make sure the IME is not on the way of preview as
510        // the user may have used it to type copies or range.
511        InputMethodManager imm = (InputMethodManager) getSystemService(
512                Context.INPUT_METHOD_SERVICE);
513        imm.hideSoftInputFromWindow(mDestinationSpinner.getWindowToken(), 0);
514    }
515
516    private void updatePrintPreviewController(boolean contentUpdated) {
517        // If we have not heard from the application, do nothing.
518        RemotePrintDocumentInfo documentInfo = mPrintedDocument.getDocumentInfo();
519        if (!documentInfo.laidout) {
520            return;
521        }
522
523        // Update the preview controller.
524        mPrintPreviewController.onContentUpdated(contentUpdated,
525                getAdjustedPageCount(documentInfo.info),
526                mPrintedDocument.getDocumentInfo().writtenPages,
527                mSelectedPages, mPrintJob.getAttributes().getMediaSize(),
528                mPrintJob.getAttributes().getMinMargins());
529    }
530
531
532    @Override
533    public boolean canOpenOptions() {
534        return true;
535    }
536
537    @Override
538    public boolean canCloseOptions() {
539        return !hasErrors();
540    }
541
542    @Override
543    public void onConfigurationChanged(Configuration newConfig) {
544        super.onConfigurationChanged(newConfig);
545        mPrintPreviewController.onOrientationChanged();
546    }
547
548    @Override
549    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
550        switch (requestCode) {
551            case ACTIVITY_REQUEST_CREATE_FILE: {
552                onStartCreateDocumentActivityResult(resultCode, data);
553            } break;
554
555            case ACTIVITY_REQUEST_SELECT_PRINTER: {
556                onSelectPrinterActivityResult(resultCode, data);
557            } break;
558
559            case ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS: {
560                onAdvancedPrintOptionsActivityResult(resultCode, data);
561            } break;
562        }
563    }
564
565    private void startCreateDocumentActivity() {
566        PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
567        if (info == null) {
568            return;
569        }
570        Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
571        intent.setType("application/pdf");
572        intent.putExtra(Intent.EXTRA_TITLE, info.getName());
573        intent.putExtra(DocumentsContract.EXTRA_PACKAGE_NAME, mCallingPackageName);
574        startActivityForResult(intent, ACTIVITY_REQUEST_CREATE_FILE);
575    }
576
577    private void onStartCreateDocumentActivityResult(int resultCode, Intent data) {
578        if (resultCode == RESULT_OK && data != null) {
579            setState(STATE_PRINT_COMPLETED);
580            updateOptionsUi();
581            final Uri uri = data.getData();
582            // Calling finish here does not invoke lifecycle callbacks but we
583            // update the print job in onPause if finishing, hence post a message.
584            mDestinationSpinner.post(new Runnable() {
585                @Override
586                public void run() {
587                    shredPagesAndFinish(uri);
588                }
589            });
590        } else if (resultCode == RESULT_CANCELED) {
591            setState(STATE_CONFIGURING);
592            updateOptionsUi();
593        } else {
594            setState(STATE_CREATE_FILE_FAILED);
595            updateOptionsUi();
596            // Calling finish here does not invoke lifecycle callbacks but we
597            // update the print job in onPause if finishing, hence post a message.
598            mDestinationSpinner.post(new Runnable() {
599                @Override
600                public void run() {
601                    finish();
602                }
603            });
604        }
605    }
606
607    private void startSelectPrinterActivity() {
608        Intent intent = new Intent(this, SelectPrinterActivity.class);
609        startActivityForResult(intent, ACTIVITY_REQUEST_SELECT_PRINTER);
610    }
611
612    private void onSelectPrinterActivityResult(int resultCode, Intent data) {
613        if (resultCode == RESULT_OK && data != null) {
614            PrinterId printerId = data.getParcelableExtra(INTENT_EXTRA_PRINTER_ID);
615            if (printerId != null) {
616                mDestinationSpinnerAdapter.ensurePrinterInVisibleAdapterPosition(printerId);
617                final int index = mDestinationSpinnerAdapter.getPrinterIndex(printerId);
618                if (index != AdapterView.INVALID_POSITION) {
619                    mDestinationSpinner.setSelection(index);
620                    return;
621                }
622            }
623        }
624
625        PrinterId printerId = mCurrentPrinter.getId();
626        final int index = mDestinationSpinnerAdapter.getPrinterIndex(printerId);
627        mDestinationSpinner.setSelection(index);
628    }
629
630    private void startAdvancedPrintOptionsActivity(PrinterInfo printer) {
631        ComponentName serviceName = printer.getId().getServiceName();
632
633        String activityName = PrintOptionUtils.getAdvancedOptionsActivityName(this, serviceName);
634        if (TextUtils.isEmpty(activityName)) {
635            return;
636        }
637
638        Intent intent = new Intent(Intent.ACTION_MAIN);
639        intent.setComponent(new ComponentName(serviceName.getPackageName(), activityName));
640
641        List<ResolveInfo> resolvedActivities = getPackageManager()
642                .queryIntentActivities(intent, 0);
643        if (resolvedActivities.isEmpty()) {
644            return;
645        }
646
647        // The activity is a component name, therefore it is one or none.
648        if (resolvedActivities.get(0).activityInfo.exported) {
649            intent.putExtra(PrintService.EXTRA_PRINT_JOB_INFO, mPrintJob);
650            intent.putExtra(PrintService.EXTRA_PRINTER_INFO, printer);
651
652            // This is external activity and may not be there.
653            try {
654                startActivityForResult(intent, ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS);
655            } catch (ActivityNotFoundException anfe) {
656                Log.e(LOG_TAG, "Error starting activity for intent: " + intent, anfe);
657            }
658        }
659    }
660
661    private void onAdvancedPrintOptionsActivityResult(int resultCode, Intent data) {
662        if (resultCode != RESULT_OK || data == null) {
663            return;
664        }
665
666        PrintJobInfo printJobInfo = data.getParcelableExtra(PrintService.EXTRA_PRINT_JOB_INFO);
667
668        if (printJobInfo == null) {
669            return;
670        }
671
672        // Take the advanced options without interpretation.
673        mPrintJob.setAdvancedOptions(printJobInfo.getAdvancedOptions());
674
675        // Take copies without interpretation as the advanced print dialog
676        // cannot create a print job info with invalid copies.
677        mCopiesEditText.setText(String.valueOf(printJobInfo.getCopies()));
678        mPrintJob.setCopies(printJobInfo.getCopies());
679
680        PrintAttributes currAttributes = mPrintJob.getAttributes();
681        PrintAttributes newAttributes = printJobInfo.getAttributes();
682
683        // Take the media size only if the current printer supports is.
684        MediaSize oldMediaSize = currAttributes.getMediaSize();
685        MediaSize newMediaSize = newAttributes.getMediaSize();
686        if (!oldMediaSize.equals(newMediaSize)) {
687            final int mediaSizeCount = mMediaSizeSpinnerAdapter.getCount();
688            MediaSize newMediaSizePortrait = newAttributes.getMediaSize().asPortrait();
689            for (int i = 0; i < mediaSizeCount; i++) {
690                MediaSize supportedSizePortrait = mMediaSizeSpinnerAdapter.getItem(i)
691                        .value.asPortrait();
692                if (supportedSizePortrait.equals(newMediaSizePortrait)) {
693                    currAttributes.setMediaSize(newMediaSize);
694                    mMediaSizeSpinner.setSelection(i);
695                    if (currAttributes.getMediaSize().isPortrait()) {
696                        if (mOrientationSpinner.getSelectedItemPosition() != 0) {
697                            mOrientationSpinner.setSelection(0);
698                        }
699                    } else {
700                        if (mOrientationSpinner.getSelectedItemPosition() != 1) {
701                            mOrientationSpinner.setSelection(1);
702                        }
703                    }
704                    break;
705                }
706            }
707        }
708
709        // Take the color mode only if the current printer supports it.
710        final int currColorMode = currAttributes.getColorMode();
711        final int newColorMode = newAttributes.getColorMode();
712        if (currColorMode != newColorMode) {
713            final int colorModeCount = mColorModeSpinner.getCount();
714            for (int i = 0; i < colorModeCount; i++) {
715                final int supportedColorMode = mColorModeSpinnerAdapter.getItem(i).value;
716                if (supportedColorMode == newColorMode) {
717                    currAttributes.setColorMode(newColorMode);
718                    mColorModeSpinner.setSelection(i);
719                    break;
720                }
721            }
722        }
723
724        PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
725        final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
726        PageRange[] pageRanges = printJobInfo.getPages();
727        updateSelectedPages(pageRanges, pageCount);
728
729        // Update the content if needed.
730        if (canUpdateDocument()) {
731            updateDocument(false);
732        }
733    }
734
735    private void setState(int state) {
736        if (isFinalState(mState)) {
737            if (isFinalState(state)) {
738                mState = state;
739            }
740        } else {
741            mState = state;
742        }
743    }
744
745    private static boolean isFinalState(int state) {
746        return state == STATE_PRINT_CONFIRMED
747                || state == STATE_PRINT_CANCELED
748                || state == STATE_PRINT_COMPLETED;
749    }
750
751    private void updateSelectedPagesFromPreview() {
752        PageRange[] selectedPages = mPrintPreviewController.getSelectedPages();
753        if (!Arrays.equals(mSelectedPages, selectedPages)) {
754            updateSelectedPages(selectedPages,
755                    getAdjustedPageCount(mPrintedDocument.getDocumentInfo().info));
756        }
757    }
758
759    private void updateSelectedPages(PageRange[] selectedPages, int pageInDocumentCount) {
760        if (selectedPages == null || selectedPages.length <= 0) {
761            return;
762        }
763
764        selectedPages = PageRangeUtils.normalize(selectedPages);
765
766        // Handle the case where all pages are specified explicitly
767        // instead of the *all pages* constant.
768        if (PageRangeUtils.isAllPages(selectedPages, pageInDocumentCount)) {
769            selectedPages = new PageRange[] {PageRange.ALL_PAGES};
770        }
771
772        if (Arrays.equals(mSelectedPages, selectedPages)) {
773            return;
774        }
775
776        mSelectedPages = selectedPages;
777        mPrintJob.setPages(selectedPages);
778
779        if (Arrays.equals(selectedPages, ALL_PAGES_ARRAY)) {
780            if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
781                mRangeOptionsSpinner.setSelection(0);
782                mPageRangeEditText.setText("");
783            }
784        } else if (selectedPages[0].getStart() >= 0
785                && selectedPages[selectedPages.length - 1].getEnd() < pageInDocumentCount) {
786            if (mRangeOptionsSpinner.getSelectedItemPosition() != 1) {
787                mRangeOptionsSpinner.setSelection(1);
788            }
789
790            StringBuilder builder = new StringBuilder();
791            final int pageRangeCount = selectedPages.length;
792            for (int i = 0; i < pageRangeCount; i++) {
793                if (builder.length() > 0) {
794                    builder.append(',');
795                }
796
797                final int shownStartPage;
798                final int shownEndPage;
799                PageRange pageRange = selectedPages[i];
800                if (pageRange.equals(PageRange.ALL_PAGES)) {
801                    shownStartPage = 1;
802                    shownEndPage = pageInDocumentCount;
803                } else {
804                    shownStartPage = pageRange.getStart() + 1;
805                    shownEndPage = pageRange.getEnd() + 1;
806                }
807
808                builder.append(shownStartPage);
809
810                if (shownStartPage != shownEndPage) {
811                    builder.append('-');
812                    builder.append(shownEndPage);
813                }
814            }
815
816            mPageRangeEditText.setText(builder.toString());
817        }
818    }
819
820    private void ensureProgressUiShown() {
821        if (mUiState != UI_STATE_PROGRESS) {
822            mUiState = UI_STATE_PROGRESS;
823            mPrintPreviewController.setUiShown(false);
824            Fragment fragment = PrintProgressFragment.newInstance();
825            showFragment(fragment);
826        }
827    }
828
829    private void ensurePreviewUiShown() {
830        if (mUiState != UI_STATE_PREVIEW) {
831            mUiState = UI_STATE_PREVIEW;
832            mPrintPreviewController.setUiShown(true);
833            showFragment(null);
834        }
835    }
836
837    private void ensureErrorUiShown(CharSequence message, int action) {
838        if (mUiState != UI_STATE_ERROR) {
839            mUiState = UI_STATE_ERROR;
840            mPrintPreviewController.setUiShown(false);
841            Fragment fragment = PrintErrorFragment.newInstance(message, action);
842            showFragment(fragment);
843        }
844    }
845
846    private void showFragment(Fragment newFragment) {
847        FragmentTransaction transaction = getFragmentManager().beginTransaction();
848        Fragment oldFragment = getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
849        if (oldFragment != null) {
850            transaction.remove(oldFragment);
851        }
852        if (newFragment != null) {
853            transaction.add(R.id.embedded_content_container, newFragment, FRAGMENT_TAG);
854        }
855        transaction.commit();
856        getFragmentManager().executePendingTransactions();
857    }
858
859    private void requestCreatePdfFileOrFinish() {
860        if (mCurrentPrinter == mDestinationSpinnerAdapter.getPdfPrinter()) {
861            startCreateDocumentActivity();
862        } else {
863            shredPagesAndFinish(null);
864        }
865    }
866
867    private void updatePrintAttributesFromCapabilities(PrinterCapabilitiesInfo capabilities) {
868        PrintAttributes defaults = capabilities.getDefaults();
869
870        // Sort the media sizes based on the current locale.
871        List<MediaSize> sortedMediaSizes = new ArrayList<>(capabilities.getMediaSizes());
872        Collections.sort(sortedMediaSizes, mMediaSizeComparator);
873
874        PrintAttributes attributes = mPrintJob.getAttributes();
875
876        // Media size.
877        MediaSize currMediaSize = attributes.getMediaSize();
878        if (currMediaSize == null) {
879            attributes.setMediaSize(defaults.getMediaSize());
880        } else {
881            boolean foundCurrentMediaSize = false;
882            // Try to find the current media size in the capabilities as
883            // it may be in a different orientation.
884            MediaSize currMediaSizePortrait = currMediaSize.asPortrait();
885            final int mediaSizeCount = sortedMediaSizes.size();
886            for (int i = 0; i < mediaSizeCount; i++) {
887                MediaSize mediaSize = sortedMediaSizes.get(i);
888                if (currMediaSizePortrait.equals(mediaSize.asPortrait())) {
889                    attributes.setMediaSize(currMediaSize);
890                    foundCurrentMediaSize = true;
891                    break;
892                }
893            }
894            // If we did not find the current media size fall back to default.
895            if (!foundCurrentMediaSize) {
896                attributes.setMediaSize(defaults.getMediaSize());
897            }
898        }
899
900        // Color mode.
901        final int colorMode = attributes.getColorMode();
902        if ((capabilities.getColorModes() & colorMode) == 0) {
903            attributes.setColorMode(defaults.getColorMode());
904        }
905
906        // Resolution
907        Resolution resolution = attributes.getResolution();
908        if (resolution == null || !capabilities.getResolutions().contains(resolution)) {
909            attributes.setResolution(defaults.getResolution());
910        }
911
912        // Margins.
913        attributes.setMinMargins(defaults.getMinMargins());
914    }
915
916    private boolean updateDocument(boolean clearLastError) {
917        if (!clearLastError && mPrintedDocument.hasUpdateError()) {
918            return false;
919        }
920
921        if (clearLastError && mPrintedDocument.hasUpdateError()) {
922            mPrintedDocument.clearUpdateError();
923        }
924
925        final boolean preview = mState != STATE_PRINT_CONFIRMED;
926        final PageRange[] pages;
927        if (preview) {
928            pages = mPrintPreviewController.getRequestedPages();
929        } else {
930            pages = mPrintPreviewController.getSelectedPages();
931        }
932
933        final boolean willUpdate = mPrintedDocument.update(mPrintJob.getAttributes(),
934                pages, preview);
935
936        if (willUpdate && !mPrintedDocument.hasLaidOutPages()) {
937            // When the update is done we update the print preview.
938            mProgressMessageController.post();
939            return true;
940        } else  {
941            // Update preview.
942            updatePrintPreviewController(false);
943        }
944
945        return false;
946    }
947
948    private void addCurrentPrinterToHistory() {
949        if (mCurrentPrinter != null) {
950            PrinterId fakePdfPrinterId = mDestinationSpinnerAdapter.getPdfPrinter().getId();
951            if (!mCurrentPrinter.getId().equals(fakePdfPrinterId)) {
952                mPrinterRegistry.addHistoricalPrinter(mCurrentPrinter);
953            }
954        }
955    }
956
957    private void cancelPrint() {
958        setState(STATE_PRINT_CANCELED);
959        updateOptionsUi();
960        if (mPrintedDocument.isUpdating()) {
961            mPrintedDocument.cancel();
962        }
963        finish();
964    }
965
966    private void confirmPrint() {
967        setState(STATE_PRINT_CONFIRMED);
968
969        updateOptionsUi();
970        addCurrentPrinterToHistory();
971
972        PageRange[] selectedPages = computeSelectedPages();
973        if (!Arrays.equals(mSelectedPages, selectedPages)) {
974            mSelectedPages = selectedPages;
975            // Update preview.
976            updatePrintPreviewController(false);
977        }
978
979        updateSelectedPagesFromPreview();
980        mPrintPreviewController.closeOptions();
981
982        if (canUpdateDocument()) {
983            updateDocument(false);
984        }
985
986        if (!mPrintedDocument.isUpdating()) {
987            requestCreatePdfFileOrFinish();
988        }
989    }
990
991    private void bindUi() {
992        // Summary
993        mSummaryCopies = (TextView) findViewById(R.id.copies_count_summary);
994        mSummaryPaperSize = (TextView) findViewById(R.id.paper_size_summary);
995
996        // Options container
997        mOptionsContent = (PrintContentView) findViewById(R.id.options_content);
998        mOptionsContent.setOptionsStateChangeListener(this);
999        mOptionsContent.setOpenOptionsController(this);
1000
1001        OnItemSelectedListener itemSelectedListener = new MyOnItemSelectedListener();
1002        OnClickListener clickListener = new MyClickListener();
1003
1004        // Copies
1005        mCopiesEditText = (EditText) findViewById(R.id.copies_edittext);
1006        mCopiesEditText.setOnFocusChangeListener(mSelectAllOnFocusListener);
1007        mCopiesEditText.setText(MIN_COPIES_STRING);
1008        mCopiesEditText.setSelection(mCopiesEditText.getText().length());
1009        mCopiesEditText.addTextChangedListener(new EditTextWatcher());
1010
1011        // Destination.
1012        mDestinationSpinnerAdapter.registerDataSetObserver(new PrintersObserver());
1013        mDestinationSpinner = (Spinner) findViewById(R.id.destination_spinner);
1014        mDestinationSpinner.setAdapter(mDestinationSpinnerAdapter);
1015        mDestinationSpinner.setOnItemSelectedListener(itemSelectedListener);
1016
1017        // Media size.
1018        mMediaSizeSpinnerAdapter = new ArrayAdapter<>(
1019                this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1020        mMediaSizeSpinner = (Spinner) findViewById(R.id.paper_size_spinner);
1021        mMediaSizeSpinner.setAdapter(mMediaSizeSpinnerAdapter);
1022        mMediaSizeSpinner.setOnItemSelectedListener(itemSelectedListener);
1023
1024        // Color mode.
1025        mColorModeSpinnerAdapter = new ArrayAdapter<>(
1026                this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1027        mColorModeSpinner = (Spinner) findViewById(R.id.color_spinner);
1028        mColorModeSpinner.setAdapter(mColorModeSpinnerAdapter);
1029        mColorModeSpinner.setOnItemSelectedListener(itemSelectedListener);
1030
1031        // Orientation
1032        mOrientationSpinnerAdapter = new ArrayAdapter<>(
1033                this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1034        String[] orientationLabels = getResources().getStringArray(
1035                R.array.orientation_labels);
1036        mOrientationSpinnerAdapter.add(new SpinnerItem<>(
1037                ORIENTATION_PORTRAIT, orientationLabels[0]));
1038        mOrientationSpinnerAdapter.add(new SpinnerItem<>(
1039                ORIENTATION_LANDSCAPE, orientationLabels[1]));
1040        mOrientationSpinner = (Spinner) findViewById(R.id.orientation_spinner);
1041        mOrientationSpinner.setAdapter(mOrientationSpinnerAdapter);
1042        mOrientationSpinner.setOnItemSelectedListener(itemSelectedListener);
1043
1044        // Range options
1045        ArrayAdapter<SpinnerItem<Integer>> rangeOptionsSpinnerAdapter = new ArrayAdapter<>(
1046                this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1047        mRangeOptionsSpinner = (Spinner) findViewById(R.id.range_options_spinner);
1048        mRangeOptionsSpinner.setAdapter(rangeOptionsSpinnerAdapter);
1049        mRangeOptionsSpinner.setOnItemSelectedListener(itemSelectedListener);
1050        updatePageRangeOptions(PrintDocumentInfo.PAGE_COUNT_UNKNOWN);
1051
1052        // Page range
1053        mPageRangeTitle = (TextView) findViewById(R.id.page_range_title);
1054        mPageRangeEditText = (EditText) findViewById(R.id.page_range_edittext);
1055        mPageRangeEditText.setOnFocusChangeListener(mSelectAllOnFocusListener);
1056        mPageRangeEditText.addTextChangedListener(new RangeTextWatcher());
1057
1058        // Advanced options button.
1059        mMoreOptionsButton = (Button) findViewById(R.id.more_options_button);
1060        mMoreOptionsButton.setOnClickListener(clickListener);
1061
1062        // Print button
1063        mPrintButton = (ImageView) findViewById(R.id.print_button);
1064        mPrintButton.setOnClickListener(clickListener);
1065    }
1066
1067    private final class MyClickListener implements OnClickListener {
1068        @Override
1069        public void onClick(View view) {
1070            if (view == mPrintButton) {
1071                if (mCurrentPrinter != null) {
1072                    confirmPrint();
1073                } else {
1074                    cancelPrint();
1075                }
1076            } else if (view == mMoreOptionsButton) {
1077                if (mCurrentPrinter != null) {
1078                    startAdvancedPrintOptionsActivity(mCurrentPrinter);
1079                }
1080            }
1081        }
1082    }
1083
1084    private static boolean canPrint(PrinterInfo printer) {
1085        return printer.getCapabilities() != null
1086                && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
1087    }
1088
1089    void updateOptionsUi() {
1090        // Always update the summary.
1091        if (!TextUtils.isEmpty(mCopiesEditText.getText())) {
1092            mSummaryCopies.setText(mCopiesEditText.getText());
1093        }
1094
1095        final int selectedMediaIndex = mMediaSizeSpinner.getSelectedItemPosition();
1096        if (selectedMediaIndex >= 0) {
1097            SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(selectedMediaIndex);
1098            mSummaryPaperSize.setText(mediaItem.label);
1099        }
1100
1101        if (mState == STATE_PRINT_CONFIRMED
1102                || mState == STATE_PRINT_COMPLETED
1103                || mState == STATE_PRINT_CANCELED
1104                || mState == STATE_UPDATE_FAILED
1105                || mState == STATE_CREATE_FILE_FAILED
1106                || mState == STATE_PRINTER_UNAVAILABLE
1107                || mState == STATE_UPDATE_SLOW) {
1108            if (mState != STATE_PRINTER_UNAVAILABLE) {
1109                mDestinationSpinner.setEnabled(false);
1110            }
1111            mCopiesEditText.setEnabled(false);
1112            mCopiesEditText.setFocusable(false);
1113            mMediaSizeSpinner.setEnabled(false);
1114            mColorModeSpinner.setEnabled(false);
1115            mOrientationSpinner.setEnabled(false);
1116            mRangeOptionsSpinner.setEnabled(false);
1117            mPageRangeEditText.setEnabled(false);
1118            mPrintButton.setVisibility(View.GONE);
1119            mMoreOptionsButton.setEnabled(false);
1120            return;
1121        }
1122
1123        // If no current printer, or it has no capabilities, or it is not
1124        // available, we disable all print options except the destination.
1125        if (mCurrentPrinter == null || !canPrint(mCurrentPrinter)) {
1126            mCopiesEditText.setEnabled(false);
1127            mCopiesEditText.setFocusable(false);
1128            mMediaSizeSpinner.setEnabled(false);
1129            mColorModeSpinner.setEnabled(false);
1130            mOrientationSpinner.setEnabled(false);
1131            mRangeOptionsSpinner.setEnabled(false);
1132            mPageRangeEditText.setEnabled(false);
1133            mPrintButton.setVisibility(View.GONE);
1134            mMoreOptionsButton.setEnabled(false);
1135            return;
1136        }
1137
1138        PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
1139        PrintAttributes defaultAttributes = capabilities.getDefaults();
1140
1141        // Destination.
1142        mDestinationSpinner.setEnabled(true);
1143
1144        // Media size.
1145        mMediaSizeSpinner.setEnabled(true);
1146
1147        List<MediaSize> mediaSizes = new ArrayList<>(capabilities.getMediaSizes());
1148        // Sort the media sizes based on the current locale.
1149        Collections.sort(mediaSizes, mMediaSizeComparator);
1150
1151        PrintAttributes attributes = mPrintJob.getAttributes();
1152
1153        // If the media sizes changed, we update the adapter and the spinner.
1154        boolean mediaSizesChanged = false;
1155        final int mediaSizeCount = mediaSizes.size();
1156        if (mediaSizeCount != mMediaSizeSpinnerAdapter.getCount()) {
1157            mediaSizesChanged = true;
1158        } else {
1159            for (int i = 0; i < mediaSizeCount; i++) {
1160                if (!mediaSizes.get(i).equals(mMediaSizeSpinnerAdapter.getItem(i).value)) {
1161                    mediaSizesChanged = true;
1162                    break;
1163                }
1164            }
1165        }
1166        if (mediaSizesChanged) {
1167            // Remember the old media size to try selecting it again.
1168            int oldMediaSizeNewIndex = AdapterView.INVALID_POSITION;
1169            MediaSize oldMediaSize = attributes.getMediaSize();
1170
1171            // Rebuild the adapter data.
1172            mMediaSizeSpinnerAdapter.clear();
1173            for (int i = 0; i < mediaSizeCount; i++) {
1174                MediaSize mediaSize = mediaSizes.get(i);
1175                if (oldMediaSize != null
1176                        && mediaSize.asPortrait().equals(oldMediaSize.asPortrait())) {
1177                    // Update the index of the old selection.
1178                    oldMediaSizeNewIndex = i;
1179                }
1180                mMediaSizeSpinnerAdapter.add(new SpinnerItem<>(
1181                        mediaSize, mediaSize.getLabel(getPackageManager())));
1182            }
1183
1184            if (oldMediaSizeNewIndex != AdapterView.INVALID_POSITION) {
1185                // Select the old media size - nothing really changed.
1186                if (mMediaSizeSpinner.getSelectedItemPosition() != oldMediaSizeNewIndex) {
1187                    mMediaSizeSpinner.setSelection(oldMediaSizeNewIndex);
1188                }
1189            } else {
1190                // Select the first or the default.
1191                final int mediaSizeIndex = Math.max(mediaSizes.indexOf(
1192                        defaultAttributes.getMediaSize()), 0);
1193                if (mMediaSizeSpinner.getSelectedItemPosition() != mediaSizeIndex) {
1194                    mMediaSizeSpinner.setSelection(mediaSizeIndex);
1195                }
1196                // Respect the orientation of the old selection.
1197                if (oldMediaSize != null) {
1198                    if (oldMediaSize.isPortrait()) {
1199                        attributes.setMediaSize(mMediaSizeSpinnerAdapter
1200                                .getItem(mediaSizeIndex).value.asPortrait());
1201                    } else {
1202                        attributes.setMediaSize(mMediaSizeSpinnerAdapter
1203                                .getItem(mediaSizeIndex).value.asLandscape());
1204                    }
1205                }
1206            }
1207        }
1208
1209        // Color mode.
1210        mColorModeSpinner.setEnabled(true);
1211        final int colorModes = capabilities.getColorModes();
1212
1213        // If the color modes changed, we update the adapter and the spinner.
1214        boolean colorModesChanged = false;
1215        if (Integer.bitCount(colorModes) != mColorModeSpinnerAdapter.getCount()) {
1216            colorModesChanged = true;
1217        } else {
1218            int remainingColorModes = colorModes;
1219            int adapterIndex = 0;
1220            while (remainingColorModes != 0) {
1221                final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
1222                final int colorMode = 1 << colorBitOffset;
1223                remainingColorModes &= ~colorMode;
1224                if (colorMode != mColorModeSpinnerAdapter.getItem(adapterIndex).value) {
1225                    colorModesChanged = true;
1226                    break;
1227                }
1228                adapterIndex++;
1229            }
1230        }
1231        if (colorModesChanged) {
1232            // Remember the old color mode to try selecting it again.
1233            int oldColorModeNewIndex = AdapterView.INVALID_POSITION;
1234            final int oldColorMode = attributes.getColorMode();
1235
1236            // Rebuild the adapter data.
1237            mColorModeSpinnerAdapter.clear();
1238            String[] colorModeLabels = getResources().getStringArray(R.array.color_mode_labels);
1239            int remainingColorModes = colorModes;
1240            while (remainingColorModes != 0) {
1241                final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
1242                final int colorMode = 1 << colorBitOffset;
1243                if (colorMode == oldColorMode) {
1244                    // Update the index of the old selection.
1245                    oldColorModeNewIndex = colorBitOffset;
1246                }
1247                remainingColorModes &= ~colorMode;
1248                mColorModeSpinnerAdapter.add(new SpinnerItem<>(colorMode,
1249                        colorModeLabels[colorBitOffset]));
1250            }
1251            if (oldColorModeNewIndex != AdapterView.INVALID_POSITION) {
1252                // Select the old color mode - nothing really changed.
1253                if (mColorModeSpinner.getSelectedItemPosition() != oldColorModeNewIndex) {
1254                    mColorModeSpinner.setSelection(oldColorModeNewIndex);
1255                }
1256            } else {
1257                // Select the default.
1258                final int selectedColorMode = colorModes & defaultAttributes.getColorMode();
1259                final int itemCount = mColorModeSpinnerAdapter.getCount();
1260                for (int i = 0; i < itemCount; i++) {
1261                    SpinnerItem<Integer> item = mColorModeSpinnerAdapter.getItem(i);
1262                    if (selectedColorMode == item.value) {
1263                        if (mColorModeSpinner.getSelectedItemPosition() != i) {
1264                            mColorModeSpinner.setSelection(i);
1265                        }
1266                        attributes.setColorMode(selectedColorMode);
1267                    }
1268                }
1269            }
1270        }
1271
1272        // Orientation
1273        mOrientationSpinner.setEnabled(true);
1274        MediaSize mediaSize = attributes.getMediaSize();
1275        if (mediaSize != null) {
1276            if (mediaSize.isPortrait()
1277                    && mOrientationSpinner.getSelectedItemPosition() != 0) {
1278                mOrientationSpinner.setSelection(0);
1279            } else if (!mediaSize.isPortrait()
1280                    && mOrientationSpinner.getSelectedItemPosition() != 1) {
1281                mOrientationSpinner.setSelection(1);
1282            }
1283        }
1284
1285        // Range options
1286        PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
1287        final int pageCount = getAdjustedPageCount(info);
1288        if (info != null && pageCount > 0) {
1289            if (pageCount == 1) {
1290                mRangeOptionsSpinner.setEnabled(false);
1291            } else {
1292                mRangeOptionsSpinner.setEnabled(true);
1293                if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
1294                    if (!mPageRangeEditText.isEnabled()) {
1295                        mPageRangeEditText.setEnabled(true);
1296                        mPageRangeEditText.setVisibility(View.VISIBLE);
1297                        mPageRangeTitle.setVisibility(View.VISIBLE);
1298                        mPageRangeEditText.requestFocus();
1299                        InputMethodManager imm = (InputMethodManager)
1300                                getSystemService(Context.INPUT_METHOD_SERVICE);
1301                        imm.showSoftInput(mPageRangeEditText, 0);
1302                    }
1303                } else {
1304                    mPageRangeEditText.setEnabled(false);
1305                    mPageRangeEditText.setVisibility(View.INVISIBLE);
1306                    mPageRangeTitle.setVisibility(View.INVISIBLE);
1307                }
1308            }
1309        } else {
1310            if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
1311                mRangeOptionsSpinner.setSelection(0);
1312                mPageRangeEditText.setText("");
1313            }
1314            mRangeOptionsSpinner.setEnabled(false);
1315            mPageRangeEditText.setEnabled(false);
1316            mPageRangeEditText.setVisibility(View.INVISIBLE);
1317            mPageRangeTitle.setVisibility(View.INVISIBLE);
1318        }
1319
1320        final int newPageCount = getAdjustedPageCount(info);
1321        if (newPageCount != mCurrentPageCount) {
1322            mCurrentPageCount = newPageCount;
1323            updatePageRangeOptions(newPageCount);
1324        }
1325
1326        // Advanced print options
1327        ComponentName serviceName = mCurrentPrinter.getId().getServiceName();
1328        if (!TextUtils.isEmpty(PrintOptionUtils.getAdvancedOptionsActivityName(
1329                this, serviceName))) {
1330            mMoreOptionsButton.setVisibility(View.VISIBLE);
1331            mMoreOptionsButton.setEnabled(true);
1332        } else {
1333            mMoreOptionsButton.setVisibility(View.GONE);
1334            mMoreOptionsButton.setEnabled(false);
1335        }
1336
1337        // Print
1338        if (mDestinationSpinnerAdapter.getPdfPrinter() != mCurrentPrinter) {
1339            mPrintButton.setImageResource(com.android.internal.R.drawable.ic_print);
1340        } else {
1341            mPrintButton.setImageResource(R.drawable.ic_menu_savetopdf);
1342        }
1343        if ((mRangeOptionsSpinner.getSelectedItemPosition() == 1
1344                && (TextUtils.isEmpty(mPageRangeEditText.getText()) || hasErrors()))
1345                || (mRangeOptionsSpinner.getSelectedItemPosition() == 0
1346                && (mPrintedDocument.getDocumentInfo() == null || hasErrors()))) {
1347            mPrintButton.setVisibility(View.GONE);
1348        } else {
1349            mPrintButton.setVisibility(View.VISIBLE);
1350        }
1351
1352        // Copies
1353        if (mDestinationSpinnerAdapter.getPdfPrinter() != mCurrentPrinter) {
1354            mCopiesEditText.setEnabled(true);
1355            mCopiesEditText.setFocusableInTouchMode(true);
1356        } else {
1357            mCopiesEditText.setEnabled(false);
1358            mCopiesEditText.setFocusable(false);
1359        }
1360        if (mCopiesEditText.getError() == null
1361                && TextUtils.isEmpty(mCopiesEditText.getText())) {
1362            mCopiesEditText.setText(String.valueOf(MIN_COPIES));
1363            mCopiesEditText.requestFocus();
1364        }
1365    }
1366
1367    private void updatePageRangeOptions(int pageCount) {
1368        ArrayAdapter<SpinnerItem<Integer>> rangeOptionsSpinnerAdapter =
1369                (ArrayAdapter) mRangeOptionsSpinner.getAdapter();
1370        rangeOptionsSpinnerAdapter.clear();
1371
1372        final int[] rangeOptionsValues = getResources().getIntArray(
1373                R.array.page_options_values);
1374
1375        String pageCountLabel = (pageCount > 0) ? String.valueOf(pageCount) : "";
1376        String[] rangeOptionsLabels = new String[] {
1377            getString(R.string.template_all_pages, pageCountLabel),
1378            getString(R.string.template_page_range, pageCountLabel)
1379        };
1380
1381        final int rangeOptionsCount = rangeOptionsLabels.length;
1382        for (int i = 0; i < rangeOptionsCount; i++) {
1383            rangeOptionsSpinnerAdapter.add(new SpinnerItem<>(
1384                    rangeOptionsValues[i], rangeOptionsLabels[i]));
1385        }
1386    }
1387
1388    private PageRange[] computeSelectedPages() {
1389        if (hasErrors()) {
1390            return null;
1391        }
1392
1393        if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
1394            List<PageRange> pageRanges = new ArrayList<>();
1395            mStringCommaSplitter.setString(mPageRangeEditText.getText().toString());
1396
1397            while (mStringCommaSplitter.hasNext()) {
1398                String range = mStringCommaSplitter.next().trim();
1399                if (TextUtils.isEmpty(range)) {
1400                    continue;
1401                }
1402                final int dashIndex = range.indexOf('-');
1403                final int fromIndex;
1404                final int toIndex;
1405
1406                if (dashIndex > 0) {
1407                    fromIndex = Integer.parseInt(range.substring(0, dashIndex).trim()) - 1;
1408                    // It is possible that the dash is at the end since the input
1409                    // verification can has to allow the user to keep entering if
1410                    // this would lead to a valid input. So we handle this.
1411                    if (dashIndex < range.length() - 1) {
1412                        String fromString = range.substring(dashIndex + 1, range.length()).trim();
1413                        toIndex = Integer.parseInt(fromString) - 1;
1414                    } else {
1415                        toIndex = fromIndex;
1416                    }
1417                } else {
1418                    fromIndex = toIndex = Integer.parseInt(range) - 1;
1419                }
1420
1421                PageRange pageRange = new PageRange(Math.min(fromIndex, toIndex),
1422                        Math.max(fromIndex, toIndex));
1423                pageRanges.add(pageRange);
1424            }
1425
1426            PageRange[] pageRangesArray = new PageRange[pageRanges.size()];
1427            pageRanges.toArray(pageRangesArray);
1428
1429            return PageRangeUtils.normalize(pageRangesArray);
1430        }
1431
1432        return ALL_PAGES_ARRAY;
1433    }
1434
1435    private int getAdjustedPageCount(PrintDocumentInfo info) {
1436        if (info != null) {
1437            final int pageCount = info.getPageCount();
1438            if (pageCount != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) {
1439                return pageCount;
1440            }
1441        }
1442        // If the app does not tell us how many pages are in the
1443        // doc we ask for all pages and use the document page count.
1444        return mPrintPreviewController.getFilePageCount();
1445    }
1446
1447    private boolean hasErrors() {
1448        return (mCopiesEditText.getError() != null)
1449                || (mPageRangeEditText.getVisibility() == View.VISIBLE
1450                && mPageRangeEditText.getError() != null);
1451    }
1452
1453    public void onPrinterAvailable(PrinterInfo printer) {
1454        if (mCurrentPrinter.equals(printer)) {
1455            setState(STATE_CONFIGURING);
1456            if (canUpdateDocument()) {
1457                updateDocument(false);
1458            }
1459            ensurePreviewUiShown();
1460            updateOptionsUi();
1461        }
1462    }
1463
1464    public void onPrinterUnavailable(PrinterInfo printer) {
1465        if (mCurrentPrinter.getId().equals(printer.getId())) {
1466            setState(STATE_PRINTER_UNAVAILABLE);
1467            if (mPrintedDocument.isUpdating()) {
1468                mPrintedDocument.cancel();
1469            }
1470            ensureErrorUiShown(getString(R.string.print_error_printer_unavailable),
1471                    PrintErrorFragment.ACTION_NONE);
1472            updateOptionsUi();
1473        }
1474    }
1475
1476    private boolean canUpdateDocument() {
1477        if (mPrintedDocument.isDestroyed()) {
1478            return false;
1479        }
1480
1481        if (hasErrors()) {
1482            return false;
1483        }
1484
1485        PrintAttributes attributes = mPrintJob.getAttributes();
1486
1487        final int colorMode = attributes.getColorMode();
1488        if (colorMode != PrintAttributes.COLOR_MODE_COLOR
1489                && colorMode != PrintAttributes.COLOR_MODE_MONOCHROME) {
1490            return false;
1491        }
1492        if (attributes.getMediaSize() == null) {
1493            return false;
1494        }
1495        if (attributes.getMinMargins() == null) {
1496            return false;
1497        }
1498        if (attributes.getResolution() == null) {
1499            return false;
1500        }
1501
1502        if (mCurrentPrinter == null) {
1503            return false;
1504        }
1505        PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
1506        if (capabilities == null) {
1507            return false;
1508        }
1509        if (mCurrentPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE) {
1510            return false;
1511        }
1512
1513        return true;
1514    }
1515
1516    private void shredPagesAndFinish(final Uri writeToUri) {
1517        new PageShredder(this, mPrintJob, mFileProvider, new Runnable() {
1518            @Override
1519            public void run() {
1520                if (writeToUri != null) {
1521                    mPrintedDocument.writeContent(getContentResolver(), writeToUri);
1522                }
1523                finish();
1524            }
1525        }).shred();
1526    }
1527
1528    private final class SpinnerItem<T> {
1529        final T value;
1530        final CharSequence label;
1531
1532        public SpinnerItem(T value, CharSequence label) {
1533            this.value = value;
1534            this.label = label;
1535        }
1536
1537        public String toString() {
1538            return label.toString();
1539        }
1540    }
1541
1542    private final class PrinterAvailabilityDetector implements Runnable {
1543        private static final long UNAVAILABLE_TIMEOUT_MILLIS = 10000; // 10sec
1544
1545        private boolean mPosted;
1546
1547        private boolean mPrinterUnavailable;
1548
1549        private PrinterInfo mPrinter;
1550
1551        public void updatePrinter(PrinterInfo printer) {
1552            if (printer.equals(mDestinationSpinnerAdapter.getPdfPrinter())) {
1553                return;
1554            }
1555
1556            final boolean available = printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE
1557                    && printer.getCapabilities() != null;
1558            final boolean notifyIfAvailable;
1559
1560            if (mPrinter == null || !mPrinter.getId().equals(printer.getId())) {
1561                notifyIfAvailable = true;
1562                unpostIfNeeded();
1563                mPrinterUnavailable = false;
1564                mPrinter = new PrinterInfo.Builder(printer).build();
1565            } else {
1566                notifyIfAvailable =
1567                        (mPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE
1568                                && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE)
1569                                || (mPrinter.getCapabilities() == null
1570                                && printer.getCapabilities() != null);
1571                mPrinter.copyFrom(printer);
1572            }
1573
1574            if (available) {
1575                unpostIfNeeded();
1576                mPrinterUnavailable = false;
1577                if (notifyIfAvailable) {
1578                    onPrinterAvailable(mPrinter);
1579                }
1580            } else {
1581                if (!mPrinterUnavailable) {
1582                    postIfNeeded();
1583                }
1584            }
1585        }
1586
1587        public void cancel() {
1588            unpostIfNeeded();
1589            mPrinterUnavailable = false;
1590        }
1591
1592        private void postIfNeeded() {
1593            if (!mPosted) {
1594                mPosted = true;
1595                mDestinationSpinner.postDelayed(this, UNAVAILABLE_TIMEOUT_MILLIS);
1596            }
1597        }
1598
1599        private void unpostIfNeeded() {
1600            if (mPosted) {
1601                mPosted = false;
1602                mDestinationSpinner.removeCallbacks(this);
1603            }
1604        }
1605
1606        @Override
1607        public void run() {
1608            mPosted = false;
1609            mPrinterUnavailable = true;
1610            onPrinterUnavailable(mPrinter);
1611        }
1612    }
1613
1614    private static final class PrinterHolder {
1615        PrinterInfo printer;
1616        boolean removed;
1617
1618        public PrinterHolder(PrinterInfo printer) {
1619            this.printer = printer;
1620        }
1621    }
1622
1623    private final class DestinationAdapter extends BaseAdapter
1624            implements PrinterRegistry.OnPrintersChangeListener {
1625        private final List<PrinterHolder> mPrinterHolders = new ArrayList<>();
1626
1627        private final PrinterHolder mFakePdfPrinterHolder;
1628
1629        private boolean mHistoricalPrintersLoaded;
1630
1631        public DestinationAdapter() {
1632            mHistoricalPrintersLoaded = mPrinterRegistry.areHistoricalPrintersLoaded();
1633            if (mHistoricalPrintersLoaded) {
1634                addPrinters(mPrinterHolders, mPrinterRegistry.getPrinters());
1635            }
1636            mPrinterRegistry.setOnPrintersChangeListener(this);
1637            mFakePdfPrinterHolder = new PrinterHolder(createFakePdfPrinter());
1638        }
1639
1640        public PrinterInfo getPdfPrinter() {
1641            return mFakePdfPrinterHolder.printer;
1642        }
1643
1644        public int getPrinterIndex(PrinterId printerId) {
1645            for (int i = 0; i < getCount(); i++) {
1646                PrinterHolder printerHolder = (PrinterHolder) getItem(i);
1647                if (printerHolder != null && !printerHolder.removed
1648                        && printerHolder.printer.getId().equals(printerId)) {
1649                    return i;
1650                }
1651            }
1652            return AdapterView.INVALID_POSITION;
1653        }
1654
1655        public void ensurePrinterInVisibleAdapterPosition(PrinterId printerId) {
1656            final int printerCount = mPrinterHolders.size();
1657            for (int i = 0; i < printerCount; i++) {
1658                PrinterHolder printerHolder = mPrinterHolders.get(i);
1659                if (printerHolder.printer.getId().equals(printerId)) {
1660                    // If already in the list - do nothing.
1661                    if (i < getCount() - 2) {
1662                        return;
1663                    }
1664                    // Else replace the last one (two items are not printers).
1665                    final int lastPrinterIndex = getCount() - 3;
1666                    mPrinterHolders.set(i, mPrinterHolders.get(lastPrinterIndex));
1667                    mPrinterHolders.set(lastPrinterIndex, printerHolder);
1668                    notifyDataSetChanged();
1669                    return;
1670                }
1671            }
1672        }
1673
1674        @Override
1675        public int getCount() {
1676            if (mHistoricalPrintersLoaded) {
1677                return Math.min(mPrinterHolders.size() + 2, DEST_ADAPTER_MAX_ITEM_COUNT);
1678            }
1679            return 0;
1680        }
1681
1682        @Override
1683        public boolean isEnabled(int position) {
1684            Object item = getItem(position);
1685            if (item instanceof PrinterHolder) {
1686                PrinterHolder printerHolder = (PrinterHolder) item;
1687                return !printerHolder.removed
1688                        && printerHolder.printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
1689            }
1690            return true;
1691        }
1692
1693        @Override
1694        public Object getItem(int position) {
1695            if (mPrinterHolders.isEmpty()) {
1696                if (position == 0) {
1697                    return mFakePdfPrinterHolder;
1698                }
1699            } else {
1700                if (position < 1) {
1701                    return mPrinterHolders.get(position);
1702                }
1703                if (position == 1) {
1704                    return mFakePdfPrinterHolder;
1705                }
1706                if (position < getCount() - 1) {
1707                    return mPrinterHolders.get(position - 1);
1708                }
1709            }
1710            return null;
1711        }
1712
1713        @Override
1714        public long getItemId(int position) {
1715            if (mPrinterHolders.isEmpty()) {
1716                if (position == 0) {
1717                    return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
1718                } else if (position == 1) {
1719                    return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS;
1720                }
1721            } else {
1722                if (position == 1) {
1723                    return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
1724                }
1725                if (position == getCount() - 1) {
1726                    return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS;
1727                }
1728            }
1729            return position;
1730        }
1731
1732        @Override
1733        public View getDropDownView(int position, View convertView, ViewGroup parent) {
1734            View view = getView(position, convertView, parent);
1735            view.setEnabled(isEnabled(position));
1736            return view;
1737        }
1738
1739        @Override
1740        public View getView(int position, View convertView, ViewGroup parent) {
1741            if (convertView == null) {
1742                convertView = getLayoutInflater().inflate(
1743                        R.layout.printer_dropdown_item, parent, false);
1744            }
1745
1746            CharSequence title = null;
1747            CharSequence subtitle = null;
1748            Drawable icon = null;
1749
1750            if (mPrinterHolders.isEmpty()) {
1751                if (position == 0 && getPdfPrinter() != null) {
1752                    PrinterHolder printerHolder = (PrinterHolder) getItem(position);
1753                    title = printerHolder.printer.getName();
1754                    icon = getResources().getDrawable(R.drawable.ic_menu_savetopdf);
1755                } else if (position == 1) {
1756                    title = getString(R.string.all_printers);
1757                }
1758            } else {
1759                if (position == 1 && getPdfPrinter() != null) {
1760                    PrinterHolder printerHolder = (PrinterHolder) getItem(position);
1761                    title = printerHolder.printer.getName();
1762                    icon = getResources().getDrawable(R.drawable.ic_menu_savetopdf);
1763                } else if (position == getCount() - 1) {
1764                    title = getString(R.string.all_printers);
1765                } else {
1766                    PrinterHolder printerHolder = (PrinterHolder) getItem(position);
1767                    title = printerHolder.printer.getName();
1768                    try {
1769                        PackageInfo packageInfo = getPackageManager().getPackageInfo(
1770                                printerHolder.printer.getId().getServiceName().getPackageName(), 0);
1771                        subtitle = packageInfo.applicationInfo.loadLabel(getPackageManager());
1772                        icon = packageInfo.applicationInfo.loadIcon(getPackageManager());
1773                    } catch (NameNotFoundException nnfe) {
1774                        /* ignore */
1775                    }
1776                }
1777            }
1778
1779            TextView titleView = (TextView) convertView.findViewById(R.id.title);
1780            titleView.setText(title);
1781
1782            TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle);
1783            if (!TextUtils.isEmpty(subtitle)) {
1784                subtitleView.setText(subtitle);
1785                subtitleView.setVisibility(View.VISIBLE);
1786            } else {
1787                subtitleView.setText(null);
1788                subtitleView.setVisibility(View.GONE);
1789            }
1790
1791            ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
1792            if (icon != null) {
1793                iconView.setImageDrawable(icon);
1794                iconView.setVisibility(View.VISIBLE);
1795            } else {
1796                iconView.setVisibility(View.INVISIBLE);
1797            }
1798
1799            return convertView;
1800        }
1801
1802        @Override
1803        public void onPrintersChanged(List<PrinterInfo> printers) {
1804            // We rearrange the printers if the user selects a printer
1805            // not shown in the initial short list. Therefore, we have
1806            // to keep the printer order.
1807
1808            // Check if historical printers are loaded as this adapter is open
1809            // for busyness only if they are. This member is updated here and
1810            // when the adapter is created because the historical printers may
1811            // be loaded before or after the adapter is created.
1812            mHistoricalPrintersLoaded = mPrinterRegistry.areHistoricalPrintersLoaded();
1813
1814            // No old printers - do not bother keeping their position.
1815            if (mPrinterHolders.isEmpty()) {
1816                addPrinters(mPrinterHolders, printers);
1817                notifyDataSetChanged();
1818                return;
1819            }
1820
1821            // Add the new printers to a map.
1822            ArrayMap<PrinterId, PrinterInfo> newPrintersMap = new ArrayMap<>();
1823            final int printerCount = printers.size();
1824            for (int i = 0; i < printerCount; i++) {
1825                PrinterInfo printer = printers.get(i);
1826                newPrintersMap.put(printer.getId(), printer);
1827            }
1828
1829            List<PrinterHolder> newPrinterHolders = new ArrayList<>();
1830
1831            // Update printers we already have which are either updated or removed.
1832            // We do not remove printers if the currently selected printer is removed
1833            // to prevent the user printing to a wrong printer.
1834            final int oldPrinterCount = mPrinterHolders.size();
1835            for (int i = 0; i < oldPrinterCount; i++) {
1836                PrinterHolder printerHolder = mPrinterHolders.get(i);
1837                PrinterId oldPrinterId = printerHolder.printer.getId();
1838                PrinterInfo updatedPrinter = newPrintersMap.remove(oldPrinterId);
1839                if (updatedPrinter != null) {
1840                    printerHolder.printer = updatedPrinter;
1841                } else {
1842                    printerHolder.removed = true;
1843                }
1844                newPrinterHolders.add(printerHolder);
1845            }
1846
1847            // Add the rest of the new printers, i.e. what is left.
1848            addPrinters(newPrinterHolders, newPrintersMap.values());
1849
1850            mPrinterHolders.clear();
1851            mPrinterHolders.addAll(newPrinterHolders);
1852
1853            notifyDataSetChanged();
1854        }
1855
1856        @Override
1857        public void onPrintersInvalid() {
1858            mPrinterHolders.clear();
1859            notifyDataSetInvalidated();
1860        }
1861
1862        public PrinterHolder getPrinterHolder(PrinterId printerId) {
1863            final int itemCount = getCount();
1864            for (int i = 0; i < itemCount; i++) {
1865                Object item = getItem(i);
1866                if (item instanceof PrinterHolder) {
1867                    PrinterHolder printerHolder = (PrinterHolder) item;
1868                    if (printerId.equals(printerHolder.printer.getId())) {
1869                        return printerHolder;
1870                    }
1871                }
1872            }
1873            return null;
1874        }
1875
1876        public void pruneRemovedPrinters() {
1877            final int holderCounts = mPrinterHolders.size();
1878            for (int i = holderCounts - 1; i >= 0; i--) {
1879                PrinterHolder printerHolder = mPrinterHolders.get(i);
1880                if (printerHolder.removed) {
1881                    mPrinterHolders.remove(i);
1882                }
1883            }
1884        }
1885
1886        private void addPrinters(List<PrinterHolder> list, Collection<PrinterInfo> printers) {
1887            for (PrinterInfo printer : printers) {
1888                PrinterHolder printerHolder = new PrinterHolder(printer);
1889                list.add(printerHolder);
1890            }
1891        }
1892
1893        private PrinterInfo createFakePdfPrinter() {
1894            MediaSize defaultMediaSize = MediaSizeUtils.getDefault(PrintActivity.this);
1895
1896            PrinterId printerId = new PrinterId(getComponentName(), "PDF printer");
1897
1898            PrinterCapabilitiesInfo.Builder builder =
1899                    new PrinterCapabilitiesInfo.Builder(printerId);
1900
1901            String[] mediaSizeIds = getResources().getStringArray(R.array.pdf_printer_media_sizes);
1902            final int mediaSizeIdCount = mediaSizeIds.length;
1903            for (int i = 0; i < mediaSizeIdCount; i++) {
1904                String id = mediaSizeIds[i];
1905                MediaSize mediaSize = MediaSize.getStandardMediaSizeById(id);
1906                builder.addMediaSize(mediaSize, mediaSize.equals(defaultMediaSize));
1907            }
1908
1909            builder.addResolution(new Resolution("PDF resolution", "PDF resolution", 300, 300),
1910                    true);
1911            builder.setColorModes(PrintAttributes.COLOR_MODE_COLOR
1912                    | PrintAttributes.COLOR_MODE_MONOCHROME, PrintAttributes.COLOR_MODE_COLOR);
1913
1914            return new PrinterInfo.Builder(printerId, getString(R.string.save_as_pdf),
1915                    PrinterInfo.STATUS_IDLE).setCapabilities(builder.build()).build();
1916        }
1917    }
1918
1919    private final class PrintersObserver extends DataSetObserver {
1920        @Override
1921        public void onChanged() {
1922            PrinterInfo oldPrinterState = mCurrentPrinter;
1923            if (oldPrinterState == null) {
1924                return;
1925            }
1926
1927            PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
1928                    oldPrinterState.getId());
1929            if (printerHolder == null) {
1930                return;
1931            }
1932            PrinterInfo newPrinterState = printerHolder.printer;
1933
1934            if (!printerHolder.removed) {
1935                mDestinationSpinnerAdapter.pruneRemovedPrinters();
1936            } else {
1937                onPrinterUnavailable(newPrinterState);
1938            }
1939
1940            if (oldPrinterState.equals(newPrinterState)) {
1941                return;
1942            }
1943
1944            PrinterCapabilitiesInfo oldCapab = oldPrinterState.getCapabilities();
1945            PrinterCapabilitiesInfo newCapab = newPrinterState.getCapabilities();
1946
1947            final boolean hasCapab = newCapab != null;
1948            final boolean gotCapab = oldCapab == null && newCapab != null;
1949            final boolean lostCapab = oldCapab != null && newCapab == null;
1950            final boolean capabChanged = capabilitiesChanged(oldCapab, newCapab);
1951
1952            final int oldStatus = oldPrinterState.getStatus();
1953            final int newStatus = newPrinterState.getStatus();
1954
1955            final boolean isActive = newStatus != PrinterInfo.STATUS_UNAVAILABLE;
1956            final boolean becameActive = (oldStatus == PrinterInfo.STATUS_UNAVAILABLE
1957                    && oldStatus != newStatus);
1958            final boolean becameInactive = (newStatus == PrinterInfo.STATUS_UNAVAILABLE
1959                    && oldStatus != newStatus);
1960
1961            mPrinterAvailabilityDetector.updatePrinter(newPrinterState);
1962
1963            oldPrinterState.copyFrom(newPrinterState);
1964
1965            if ((isActive && gotCapab) || (becameActive && hasCapab)) {
1966                if (hasCapab && capabChanged) {
1967                    updatePrintAttributesFromCapabilities(newCapab);
1968                }
1969                onPrinterAvailable(newPrinterState);
1970            } else if ((becameInactive && hasCapab) || (isActive && lostCapab)) {
1971                onPrinterUnavailable(newPrinterState);
1972            }
1973
1974            final boolean updateNeeded = ((capabChanged && hasCapab && isActive)
1975                    || (becameActive && hasCapab) || (isActive && gotCapab));
1976
1977            if (updateNeeded && canUpdateDocument()) {
1978                updateDocument(false);
1979            }
1980
1981            updateOptionsUi();
1982        }
1983
1984        private boolean capabilitiesChanged(PrinterCapabilitiesInfo oldCapabilities,
1985                PrinterCapabilitiesInfo newCapabilities) {
1986            if (oldCapabilities == null) {
1987                if (newCapabilities != null) {
1988                    return true;
1989                }
1990            } else if (!oldCapabilities.equals(newCapabilities)) {
1991                return true;
1992            }
1993            return false;
1994        }
1995    }
1996
1997    private final class MyOnItemSelectedListener implements AdapterView.OnItemSelectedListener {
1998        @Override
1999        public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) {
2000            if (spinner == mDestinationSpinner) {
2001                if (position == AdapterView.INVALID_POSITION) {
2002                    return;
2003                }
2004
2005                if (id == DEST_ADAPTER_ITEM_ID_ALL_PRINTERS) {
2006                    startSelectPrinterActivity();
2007                    return;
2008                }
2009
2010                PrinterHolder currentItem = (PrinterHolder) mDestinationSpinner.getSelectedItem();
2011                PrinterInfo currentPrinter = (currentItem != null) ? currentItem.printer : null;
2012
2013                // Why on earth item selected is called if no selection changed.
2014                if (mCurrentPrinter == currentPrinter) {
2015                    return;
2016                }
2017
2018                mCurrentPrinter = currentPrinter;
2019
2020                PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
2021                        currentPrinter.getId());
2022                if (!printerHolder.removed) {
2023                    setState(STATE_CONFIGURING);
2024                    mDestinationSpinnerAdapter.pruneRemovedPrinters();
2025                    ensurePreviewUiShown();
2026                }
2027
2028                mPrintJob.setPrinterId(currentPrinter.getId());
2029                mPrintJob.setPrinterName(currentPrinter.getName());
2030
2031                mPrinterRegistry.setTrackedPrinter(currentPrinter.getId());
2032
2033                PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities();
2034                if (capabilities != null) {
2035                    updatePrintAttributesFromCapabilities(capabilities);
2036                }
2037
2038                mPrinterAvailabilityDetector.updatePrinter(currentPrinter);
2039            } else if (spinner == mMediaSizeSpinner) {
2040                SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position);
2041                PrintAttributes attributes = mPrintJob.getAttributes();
2042                if (mOrientationSpinner.getSelectedItemPosition() == 0) {
2043                    attributes.setMediaSize(mediaItem.value.asPortrait());
2044                } else {
2045                    attributes.setMediaSize(mediaItem.value.asLandscape());
2046                }
2047            } else if (spinner == mColorModeSpinner) {
2048                SpinnerItem<Integer> colorModeItem = mColorModeSpinnerAdapter.getItem(position);
2049                mPrintJob.getAttributes().setColorMode(colorModeItem.value);
2050            } else if (spinner == mOrientationSpinner) {
2051                SpinnerItem<Integer> orientationItem = mOrientationSpinnerAdapter.getItem(position);
2052                PrintAttributes attributes = mPrintJob.getAttributes();
2053                if (mMediaSizeSpinner.getSelectedItem() != null) {
2054                    if (orientationItem.value == ORIENTATION_PORTRAIT) {
2055                        attributes.copyFrom(attributes.asPortrait());
2056                    } else {
2057                        attributes.copyFrom(attributes.asLandscape());
2058                    }
2059                }
2060            } else if (spinner == mRangeOptionsSpinner) {
2061                if (mRangeOptionsSpinner.getSelectedItemPosition() == 0) {
2062                    mPageRangeEditText.setText("");
2063                } else if (TextUtils.isEmpty(mPageRangeEditText.getText())) {
2064                    mPageRangeEditText.setError("");
2065                }
2066            }
2067
2068            if (canUpdateDocument()) {
2069                updateDocument(false);
2070            }
2071
2072            updateOptionsUi();
2073        }
2074
2075        @Override
2076        public void onNothingSelected(AdapterView<?> parent) {
2077            /* do nothing*/
2078        }
2079    }
2080
2081    private final class SelectAllOnFocusListener implements OnFocusChangeListener {
2082        @Override
2083        public void onFocusChange(View view, boolean hasFocus) {
2084            EditText editText = (EditText) view;
2085            if (!TextUtils.isEmpty(editText.getText())) {
2086                editText.setSelection(editText.getText().length());
2087            }
2088        }
2089    }
2090
2091    private final class RangeTextWatcher implements TextWatcher {
2092        @Override
2093        public void onTextChanged(CharSequence s, int start, int before, int count) {
2094            /* do nothing */
2095        }
2096
2097        @Override
2098        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2099            /* do nothing */
2100        }
2101
2102        @Override
2103        public void afterTextChanged(Editable editable) {
2104            final boolean hadErrors = hasErrors();
2105
2106            String text = editable.toString();
2107
2108            if (TextUtils.isEmpty(text)) {
2109                mPageRangeEditText.setError("");
2110                updateOptionsUi();
2111                return;
2112            }
2113
2114            String escapedText = PATTERN_ESCAPE_SPECIAL_CHARS.matcher(text).replaceAll("////");
2115            if (!PATTERN_PAGE_RANGE.matcher(escapedText).matches()) {
2116                mPageRangeEditText.setError("");
2117                updateOptionsUi();
2118                return;
2119            }
2120
2121            PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
2122            final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
2123
2124            // The range
2125            Matcher matcher = PATTERN_DIGITS.matcher(text);
2126            while (matcher.find()) {
2127                String numericString = text.substring(matcher.start(), matcher.end()).trim();
2128                if (TextUtils.isEmpty(numericString)) {
2129                    continue;
2130                }
2131                final int pageIndex = Integer.parseInt(numericString);
2132                if (pageIndex < 1 || pageIndex > pageCount) {
2133                    mPageRangeEditText.setError("");
2134                    updateOptionsUi();
2135                    return;
2136                }
2137            }
2138
2139            // We intentionally do not catch the case of the from page being
2140            // greater than the to page. When computing the requested pages
2141            // we just swap them if necessary.
2142
2143            mPageRangeEditText.setError(null);
2144            mPrintButton.setEnabled(true);
2145            updateOptionsUi();
2146
2147            if (hadErrors && !hasErrors()) {
2148                updateOptionsUi();
2149            }
2150        }
2151    }
2152
2153    private final class EditTextWatcher implements TextWatcher {
2154        @Override
2155        public void onTextChanged(CharSequence s, int start, int before, int count) {
2156            /* do nothing */
2157        }
2158
2159        @Override
2160        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2161            /* do nothing */
2162        }
2163
2164        @Override
2165        public void afterTextChanged(Editable editable) {
2166            final boolean hadErrors = hasErrors();
2167
2168            if (editable.length() == 0) {
2169                mCopiesEditText.setError("");
2170                updateOptionsUi();
2171                return;
2172            }
2173
2174            int copies = 0;
2175            try {
2176                copies = Integer.parseInt(editable.toString());
2177            } catch (NumberFormatException nfe) {
2178                /* ignore */
2179            }
2180
2181            if (copies < MIN_COPIES) {
2182                mCopiesEditText.setError("");
2183                updateOptionsUi();
2184                return;
2185            }
2186
2187            mPrintJob.setCopies(copies);
2188
2189            mCopiesEditText.setError(null);
2190
2191            updateOptionsUi();
2192
2193            if (hadErrors && canUpdateDocument()) {
2194                updateDocument(false);
2195            }
2196        }
2197    }
2198
2199    private final class ProgressMessageController implements Runnable {
2200        private static final long PROGRESS_TIMEOUT_MILLIS = 1000;
2201
2202        private final Handler mHandler;
2203
2204        private boolean mPosted;
2205
2206        public ProgressMessageController(Context context) {
2207            mHandler = new Handler(context.getMainLooper(), null, false);
2208        }
2209
2210        public void post() {
2211            if (mPosted) {
2212                return;
2213            }
2214            mPosted = true;
2215            mHandler.postDelayed(this, PROGRESS_TIMEOUT_MILLIS);
2216        }
2217
2218        public void cancel() {
2219            if (!mPosted) {
2220                return;
2221            }
2222            mPosted = false;
2223            mHandler.removeCallbacks(this);
2224        }
2225
2226        @Override
2227        public void run() {
2228            mPosted = false;
2229            setState(STATE_UPDATE_SLOW);
2230            ensureProgressUiShown();
2231            updateOptionsUi();
2232        }
2233    }
2234
2235    private static final class PageShredder implements ServiceConnection {
2236        private static final String TEMP_FILE_PREFIX = "print_job";
2237        private static final String TEMP_FILE_EXTENSION = ".pdf";
2238
2239        private final Context mContext;
2240
2241        private final MutexFileProvider mFileProvider;
2242
2243        private final PrintJobInfo mPrintJob;
2244
2245        private final PageRange[] mPagesToShred;
2246
2247        private final Runnable mCallback;
2248
2249        public PageShredder(Context context, PrintJobInfo printJob,
2250                MutexFileProvider fileProvider, Runnable callback) {
2251            mContext = context;
2252            mPrintJob = printJob;
2253            mFileProvider = fileProvider;
2254            mCallback = callback;
2255            mPagesToShred = computePagesToShred(mPrintJob);
2256        }
2257
2258        public void shred() {
2259            // If we have only the pages we want, done.
2260            if (mPagesToShred.length <= 0) {
2261                mCallback.run();
2262                return;
2263            }
2264
2265            // Bind to the manipulation service and the work
2266            // will be performed upon connection to the service.
2267            Intent intent = new Intent(PdfManipulationService.ACTION_GET_EDITOR);
2268            intent.setClass(mContext, PdfManipulationService.class);
2269            mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
2270        }
2271
2272        @Override
2273        public void onServiceConnected(ComponentName name, IBinder service) {
2274            final IPdfEditor editor = IPdfEditor.Stub.asInterface(service);
2275            new AsyncTask<Void, Void, Void>() {
2276                @Override
2277                protected Void doInBackground(Void... params) {
2278                    try {
2279                        // It's OK to access the data members as they are
2280                        // final and this code is the last one to touch
2281                        // them as shredding is the very last step, so the
2282                        // UI is not interactive at this point.
2283                        shredPages(editor);
2284                        updatePrintJob();
2285                    } finally {
2286                        mContext.unbindService(PageShredder.this);
2287                        mCallback.run();
2288                    }
2289                    return null;
2290                }
2291            }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
2292        }
2293
2294        @Override
2295        public void onServiceDisconnected(ComponentName name) {
2296            /* do nothing */
2297        }
2298
2299        private void shredPages(IPdfEditor editor) {
2300            File tempFile = null;
2301            ParcelFileDescriptor src = null;
2302            ParcelFileDescriptor dst = null;
2303            InputStream in = null;
2304            OutputStream out = null;
2305            try {
2306                File jobFile = mFileProvider.acquireFile(null);
2307                src = ParcelFileDescriptor.open(jobFile, ParcelFileDescriptor.MODE_READ_WRITE);
2308
2309                // Open the document.
2310                editor.openDocument(src);
2311
2312                // We passed the fd over IPC, close this one.
2313                src.close();
2314
2315                // Drop the pages.
2316                editor.removePages(mPagesToShred);
2317
2318                // Write the modified PDF to a temp file.
2319                tempFile = File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_EXTENSION,
2320                        mContext.getCacheDir());
2321                dst = ParcelFileDescriptor.open(tempFile, ParcelFileDescriptor.MODE_READ_WRITE);
2322                editor.write(dst);
2323                dst.close();
2324
2325                // Close the document.
2326                editor.closeDocument();
2327
2328                // Copy the temp file over the print job file.
2329                jobFile.delete();
2330                in = new FileInputStream(tempFile);
2331                out = new FileOutputStream(jobFile);
2332                Streams.copy(in, out);
2333            } catch (IOException|RemoteException e) {
2334                Log.e(LOG_TAG, "Error dropping pages", e);
2335            } finally {
2336                IoUtils.closeQuietly(src);
2337                IoUtils.closeQuietly(dst);
2338                IoUtils.closeQuietly(in);
2339                IoUtils.closeQuietly(out);
2340                if (tempFile != null) {
2341                    tempFile.delete();
2342                }
2343            }
2344        }
2345
2346        private void updatePrintJob() {
2347            // Update the print job pages.
2348            final int newPageCount = PageRangeUtils.getNormalizedPageCount(
2349                    mPrintJob.getPages(), 0);
2350            mPrintJob.setPages(new PageRange[]{PageRange.ALL_PAGES});
2351
2352            // Update the print job document info.
2353            PrintDocumentInfo oldDocInfo = mPrintJob.getDocumentInfo();
2354            PrintDocumentInfo newDocInfo = new PrintDocumentInfo
2355                    .Builder(oldDocInfo.getName())
2356                    .setContentType(oldDocInfo.getContentType())
2357                    .setPageCount(newPageCount)
2358                    .build();
2359            mPrintJob.setDocumentInfo(newDocInfo);
2360        }
2361
2362        private static PageRange[] computePagesToShred(PrintJobInfo printJob) {
2363            List<PageRange> rangesToShred = new ArrayList<>();
2364            PageRange previousRange = null;
2365
2366            final int pageCount = printJob.getDocumentInfo().getPageCount();
2367
2368            PageRange[] printedPages = printJob.getPages();
2369            final int rangeCount = printedPages.length;
2370            for (int i = 0; i < rangeCount; i++) {
2371                PageRange range = PageRangeUtils.asAbsoluteRange(printedPages[i], pageCount);
2372
2373                if (previousRange == null) {
2374                    final int startPageIdx = 0;
2375                    final int endPageIdx = range.getStart() - 1;
2376                    if (startPageIdx <= endPageIdx) {
2377                        PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
2378                        rangesToShred.add(removedRange);
2379                    }
2380                } else {
2381                    final int startPageIdx = previousRange.getEnd() + 1;
2382                    final int endPageIdx = range.getStart() - 1;
2383                    if (startPageIdx <= endPageIdx) {
2384                        PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
2385                        rangesToShred.add(removedRange);
2386                    }
2387                }
2388
2389                if (i == rangeCount - 1) {
2390                    final int startPageIdx = range.getEnd() + 1;
2391                    final int endPageIdx = printJob.getDocumentInfo().getPageCount() - 1;
2392                    if (startPageIdx <= endPageIdx) {
2393                        PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
2394                        rangesToShred.add(removedRange);
2395                    }
2396                }
2397
2398                previousRange = range;
2399            }
2400
2401            PageRange[] result = new PageRange[rangesToShred.size()];
2402            rangesToShred.toArray(result);
2403            return result;
2404        }
2405    }
2406}
2407