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