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