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