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