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