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