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