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