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