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