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