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