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