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