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