• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
789                 mDestinationSpinnerAdapter.ensurePrinterInVisibleAdapterPosition(printerInfo);
790 
791                 MetricsLogger.action(this, MetricsEvent.ACTION_PRINTER_SELECT_ALL,
792                         printerInfo.getId().getServiceName().getPackageName());
793             }
794         }
795 
796         if (mCurrentPrinter != null) {
797             // Trigger PrintersObserver.onChanged() to adjust selection back to current printer
798             mDestinationSpinnerAdapter.notifyDataSetChanged();
799         }
800     }
801 
startAdvancedPrintOptionsActivity(PrinterInfo printer)802     private void startAdvancedPrintOptionsActivity(PrinterInfo printer) {
803         if (mAdvancedPrintOptionsActivity == null) {
804             return;
805         }
806 
807         Intent intent = new Intent(Intent.ACTION_MAIN);
808         intent.setComponent(mAdvancedPrintOptionsActivity);
809 
810         List<ResolveInfo> resolvedActivities = getPackageManager()
811                 .queryIntentActivities(intent, 0);
812         if (resolvedActivities.isEmpty()) {
813             Log.w(LOG_TAG, "Advanced options activity " + mAdvancedPrintOptionsActivity + " could "
814                     + "not be found");
815             return;
816         }
817 
818         // The activity is a component name, therefore it is one or none.
819         if (resolvedActivities.get(0).activityInfo.exported) {
820             PrintJobInfo.Builder printJobBuilder = new PrintJobInfo.Builder(mPrintJob);
821             printJobBuilder.setPages(mSelectedPages);
822 
823             intent.putExtra(PrintService.EXTRA_PRINT_JOB_INFO, printJobBuilder.build());
824             intent.putExtra(PrintService.EXTRA_PRINTER_INFO, printer);
825             intent.putExtra(PrintService.EXTRA_PRINT_DOCUMENT_INFO,
826                     mPrintedDocument.getDocumentInfo().info);
827 
828             mIsMoreOptionsActivityInProgress = true;
829 
830             // This is external activity and may not be there.
831             try {
832                 startActivityForResult(intent, ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS);
833             } catch (ActivityNotFoundException anfe) {
834                 mIsMoreOptionsActivityInProgress = false;
835                 Log.e(LOG_TAG, "Error starting activity for intent: " + intent, anfe);
836             }
837 
838             mMoreOptionsButton.setEnabled(!mIsMoreOptionsActivityInProgress);
839         }
840     }
841 
onAdvancedPrintOptionsActivityResult(int resultCode, Intent data)842     private void onAdvancedPrintOptionsActivityResult(int resultCode, Intent data) {
843         mIsMoreOptionsActivityInProgress = false;
844         mMoreOptionsButton.setEnabled(true);
845 
846         if (resultCode != RESULT_OK || data == null) {
847             return;
848         }
849 
850         PrintJobInfo printJobInfo = data.getParcelableExtra(PrintService.EXTRA_PRINT_JOB_INFO);
851 
852         if (printJobInfo == null) {
853             return;
854         }
855 
856         // Take the advanced options without interpretation.
857         mPrintJob.setAdvancedOptions(printJobInfo.getAdvancedOptions());
858 
859         if (printJobInfo.getCopies() < 1) {
860             Log.w(LOG_TAG, "Cannot apply return value from advanced options activity. Copies " +
861                     "must be 1 or more. Actual value is: " + printJobInfo.getCopies() + ". " +
862                     "Ignoring.");
863         } else {
864             mCopiesEditText.setText(String.valueOf(printJobInfo.getCopies()));
865             mPrintJob.setCopies(printJobInfo.getCopies());
866         }
867 
868         PrintAttributes currAttributes = mPrintJob.getAttributes();
869         PrintAttributes newAttributes = printJobInfo.getAttributes();
870 
871         if (newAttributes != null) {
872             // Take the media size only if the current printer supports is.
873             MediaSize oldMediaSize = currAttributes.getMediaSize();
874             MediaSize newMediaSize = newAttributes.getMediaSize();
875             if (newMediaSize != null && !oldMediaSize.equals(newMediaSize)) {
876                 final int mediaSizeCount = mMediaSizeSpinnerAdapter.getCount();
877                 MediaSize newMediaSizePortrait = newAttributes.getMediaSize().asPortrait();
878                 for (int i = 0; i < mediaSizeCount; i++) {
879                     MediaSize supportedSizePortrait = mMediaSizeSpinnerAdapter.getItem(i)
880                             .value.asPortrait();
881                     if (supportedSizePortrait.equals(newMediaSizePortrait)) {
882                         currAttributes.setMediaSize(newMediaSize);
883                         mMediaSizeSpinner.setSelection(i);
884                         if (currAttributes.getMediaSize().isPortrait()) {
885                             if (mOrientationSpinner.getSelectedItemPosition() != 0) {
886                                 mOrientationSpinner.setSelection(0);
887                             }
888                         } else {
889                             if (mOrientationSpinner.getSelectedItemPosition() != 1) {
890                                 mOrientationSpinner.setSelection(1);
891                             }
892                         }
893                         break;
894                     }
895                 }
896             }
897 
898             // Take the resolution only if the current printer supports is.
899             Resolution oldResolution = currAttributes.getResolution();
900             Resolution newResolution = newAttributes.getResolution();
901             if (!oldResolution.equals(newResolution)) {
902                 PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
903                 if (capabilities != null) {
904                     List<Resolution> resolutions = capabilities.getResolutions();
905                     final int resolutionCount = resolutions.size();
906                     for (int i = 0; i < resolutionCount; i++) {
907                         Resolution resolution = resolutions.get(i);
908                         if (resolution.equals(newResolution)) {
909                             currAttributes.setResolution(resolution);
910                             break;
911                         }
912                     }
913                 }
914             }
915 
916             // Take the color mode only if the current printer supports it.
917             final int currColorMode = currAttributes.getColorMode();
918             final int newColorMode = newAttributes.getColorMode();
919             if (currColorMode != newColorMode) {
920                 final int colorModeCount = mColorModeSpinner.getCount();
921                 for (int i = 0; i < colorModeCount; i++) {
922                     final int supportedColorMode = mColorModeSpinnerAdapter.getItem(i).value;
923                     if (supportedColorMode == newColorMode) {
924                         currAttributes.setColorMode(newColorMode);
925                         mColorModeSpinner.setSelection(i);
926                         break;
927                     }
928                 }
929             }
930 
931             // Take the duplex mode only if the current printer supports it.
932             final int currDuplexMode = currAttributes.getDuplexMode();
933             final int newDuplexMode = newAttributes.getDuplexMode();
934             if (currDuplexMode != newDuplexMode) {
935                 final int duplexModeCount = mDuplexModeSpinner.getCount();
936                 for (int i = 0; i < duplexModeCount; i++) {
937                     final int supportedDuplexMode = mDuplexModeSpinnerAdapter.getItem(i).value;
938                     if (supportedDuplexMode == newDuplexMode) {
939                         currAttributes.setDuplexMode(newDuplexMode);
940                         mDuplexModeSpinner.setSelection(i);
941                         break;
942                     }
943                 }
944             }
945         }
946 
947         // Handle selected page changes making sure they are in the doc.
948         PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
949         final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
950         PageRange[] pageRanges = printJobInfo.getPages();
951         if (pageRanges != null && pageCount > 0) {
952             pageRanges = PageRangeUtils.normalize(pageRanges);
953 
954             List<PageRange> validatedList = new ArrayList<>();
955             final int rangeCount = pageRanges.length;
956             for (int i = 0; i < rangeCount; i++) {
957                 PageRange pageRange = pageRanges[i];
958                 if (pageRange.getEnd() >= pageCount) {
959                     final int rangeStart = pageRange.getStart();
960                     final int rangeEnd = pageCount - 1;
961                     if (rangeStart <= rangeEnd) {
962                         pageRange = new PageRange(rangeStart, rangeEnd);
963                         validatedList.add(pageRange);
964                     }
965                     break;
966                 }
967                 validatedList.add(pageRange);
968             }
969 
970             if (!validatedList.isEmpty()) {
971                 PageRange[] validatedArray = new PageRange[validatedList.size()];
972                 validatedList.toArray(validatedArray);
973                 updateSelectedPages(validatedArray, pageCount);
974             }
975         }
976 
977         // Update the content if needed.
978         if (canUpdateDocument()) {
979             updateDocument(false);
980         }
981     }
982 
setState(int state)983     private void setState(int state) {
984         if (isFinalState(mState)) {
985             if (isFinalState(state)) {
986                 if (DEBUG) {
987                     Log.i(LOG_TAG, "[state]" + state);
988                 }
989                 mState = state;
990                 updateOptionsUi();
991             }
992         } else {
993             if (DEBUG) {
994                 Log.i(LOG_TAG, "[state]" + state);
995             }
996             mState = state;
997             updateOptionsUi();
998         }
999     }
1000 
isFinalState(int state)1001     private static boolean isFinalState(int state) {
1002         return state == STATE_PRINT_CANCELED
1003                 || state == STATE_PRINT_COMPLETED
1004                 || state == STATE_CREATE_FILE_FAILED;
1005     }
1006 
updateSelectedPagesFromPreview()1007     private void updateSelectedPagesFromPreview() {
1008         PageRange[] selectedPages = mPrintPreviewController.getSelectedPages();
1009         if (!Arrays.equals(mSelectedPages, selectedPages)) {
1010             updateSelectedPages(selectedPages,
1011                     getAdjustedPageCount(mPrintedDocument.getDocumentInfo().info));
1012         }
1013     }
1014 
updateSelectedPages(PageRange[] selectedPages, int pageInDocumentCount)1015     private void updateSelectedPages(PageRange[] selectedPages, int pageInDocumentCount) {
1016         if (selectedPages == null || selectedPages.length <= 0) {
1017             return;
1018         }
1019 
1020         selectedPages = PageRangeUtils.normalize(selectedPages);
1021 
1022         // Handle the case where all pages are specified explicitly
1023         // instead of the *all pages* constant.
1024         if (PageRangeUtils.isAllPages(selectedPages, pageInDocumentCount)) {
1025             selectedPages = new PageRange[] {PageRange.ALL_PAGES};
1026         }
1027 
1028         if (Arrays.equals(mSelectedPages, selectedPages)) {
1029             return;
1030         }
1031 
1032         mSelectedPages = selectedPages;
1033         mPrintJob.setPages(selectedPages);
1034 
1035         if (Arrays.equals(selectedPages, PageRange.ALL_PAGES_ARRAY)) {
1036             if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
1037                 mRangeOptionsSpinner.setSelection(0);
1038                 mPageRangeEditText.setText("");
1039             }
1040         } else if (selectedPages[0].getStart() >= 0
1041                 && selectedPages[selectedPages.length - 1].getEnd() < pageInDocumentCount) {
1042             if (mRangeOptionsSpinner.getSelectedItemPosition() != 1) {
1043                 mRangeOptionsSpinner.setSelection(1);
1044             }
1045 
1046             StringBuilder builder = new StringBuilder();
1047             final int pageRangeCount = selectedPages.length;
1048             for (int i = 0; i < pageRangeCount; i++) {
1049                 if (builder.length() > 0) {
1050                     builder.append(',');
1051                 }
1052 
1053                 final int shownStartPage;
1054                 final int shownEndPage;
1055                 PageRange pageRange = selectedPages[i];
1056                 if (pageRange.equals(PageRange.ALL_PAGES)) {
1057                     shownStartPage = 1;
1058                     shownEndPage = pageInDocumentCount;
1059                 } else {
1060                     shownStartPage = pageRange.getStart() + 1;
1061                     shownEndPage = pageRange.getEnd() + 1;
1062                 }
1063 
1064                 builder.append(shownStartPage);
1065 
1066                 if (shownStartPage != shownEndPage) {
1067                     builder.append('-');
1068                     builder.append(shownEndPage);
1069                 }
1070             }
1071 
1072             mPageRangeEditText.setText(builder.toString());
1073         }
1074     }
1075 
ensureProgressUiShown()1076     private void ensureProgressUiShown() {
1077         if (isFinishing() || isDestroyed()) {
1078             return;
1079         }
1080         if (mUiState != UI_STATE_PROGRESS) {
1081             mUiState = UI_STATE_PROGRESS;
1082             mPrintPreviewController.setUiShown(false);
1083             Fragment fragment = PrintProgressFragment.newInstance();
1084             showFragment(fragment);
1085         }
1086     }
1087 
ensurePreviewUiShown()1088     private void ensurePreviewUiShown() {
1089         if (isFinishing() || isDestroyed()) {
1090             return;
1091         }
1092         if (mUiState != UI_STATE_PREVIEW) {
1093             mUiState = UI_STATE_PREVIEW;
1094             mPrintPreviewController.setUiShown(true);
1095             showFragment(null);
1096         }
1097     }
1098 
ensureErrorUiShown(CharSequence message, int action)1099     private void ensureErrorUiShown(CharSequence message, int action) {
1100         if (isFinishing() || isDestroyed()) {
1101             return;
1102         }
1103         if (mUiState != UI_STATE_ERROR) {
1104             mUiState = UI_STATE_ERROR;
1105             mPrintPreviewController.setUiShown(false);
1106             Fragment fragment = PrintErrorFragment.newInstance(message, action);
1107             showFragment(fragment);
1108         }
1109     }
1110 
showFragment(Fragment newFragment)1111     private void showFragment(Fragment newFragment) {
1112         FragmentTransaction transaction = getFragmentManager().beginTransaction();
1113         Fragment oldFragment = getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
1114         if (oldFragment != null) {
1115             transaction.remove(oldFragment);
1116         }
1117         if (newFragment != null) {
1118             transaction.add(R.id.embedded_content_container, newFragment, FRAGMENT_TAG);
1119         }
1120         transaction.commitAllowingStateLoss();
1121         getFragmentManager().executePendingTransactions();
1122     }
1123 
1124     /**
1125      * Count that a print operation has been confirmed.
1126      *
1127      * @param packageName The package name of the print service used
1128      */
countPrintOperation(@onNull String packageName)1129     private void countPrintOperation(@NonNull String packageName) {
1130         MetricsLogger.action(this, MetricsEvent.ACTION_PRINT, packageName);
1131 
1132         MetricsLogger.histogram(this, PRINT_PAGES_HISTO,
1133                 getAdjustedPageCount(mPrintJob.getDocumentInfo()));
1134 
1135         if (mPrintJob.getPrinterId().equals(mDefaultPrinter)) {
1136             MetricsLogger.histogram(this, PRINT_DEFAULT_COUNT, 1);
1137         }
1138 
1139         UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
1140         if (um.isManagedProfile()) {
1141             MetricsLogger.histogram(this, PRINT_WORK_COUNT, 1);
1142         }
1143     }
1144 
requestCreatePdfFileOrFinish()1145     private void requestCreatePdfFileOrFinish() {
1146         mPrintedDocument.cancel(false);
1147 
1148         if (mCurrentPrinter == mDestinationSpinnerAdapter.getPdfPrinter()) {
1149             startCreateDocumentActivity();
1150         } else {
1151             countPrintOperation(mCurrentPrinter.getId().getServiceName().getPackageName());
1152 
1153             transformDocumentAndFinish(null);
1154         }
1155     }
1156 
1157     /**
1158      * Clear the selected page range and update the preview if needed.
1159      */
clearPageRanges()1160     private void clearPageRanges() {
1161         mRangeOptionsSpinner.setSelection(0);
1162         mPageRangeEditText.setError(null);
1163         mPageRangeEditText.setText("");
1164         mSelectedPages = PageRange.ALL_PAGES_ARRAY;
1165 
1166         if (!Arrays.equals(mSelectedPages, mPrintPreviewController.getSelectedPages())) {
1167             updatePrintPreviewController(false);
1168         }
1169     }
1170 
updatePrintAttributesFromCapabilities(PrinterCapabilitiesInfo capabilities)1171     private void updatePrintAttributesFromCapabilities(PrinterCapabilitiesInfo capabilities) {
1172         boolean clearRanges = false;
1173         PrintAttributes defaults = capabilities.getDefaults();
1174 
1175         // Sort the media sizes based on the current locale.
1176         List<MediaSize> sortedMediaSizes = new ArrayList<>(capabilities.getMediaSizes());
1177         Collections.sort(sortedMediaSizes, mMediaSizeComparator);
1178 
1179         PrintAttributes attributes = mPrintJob.getAttributes();
1180 
1181         // Media size.
1182         MediaSize currMediaSize = attributes.getMediaSize();
1183         if (currMediaSize == null) {
1184             clearRanges = true;
1185             attributes.setMediaSize(defaults.getMediaSize());
1186         } else {
1187             MediaSize newMediaSize = null;
1188             boolean isPortrait = currMediaSize.isPortrait();
1189 
1190             // Try to find the current media size in the capabilities as
1191             // it may be in a different orientation.
1192             MediaSize currMediaSizePortrait = currMediaSize.asPortrait();
1193             final int mediaSizeCount = sortedMediaSizes.size();
1194             for (int i = 0; i < mediaSizeCount; i++) {
1195                 MediaSize mediaSize = sortedMediaSizes.get(i);
1196                 if (currMediaSizePortrait.equals(mediaSize.asPortrait())) {
1197                     newMediaSize = mediaSize;
1198                     break;
1199                 }
1200             }
1201             // If we did not find the current media size fall back to default.
1202             if (newMediaSize == null) {
1203                 clearRanges = true;
1204                 newMediaSize = defaults.getMediaSize();
1205             }
1206 
1207             if (newMediaSize != null) {
1208                 if (isPortrait) {
1209                     attributes.setMediaSize(newMediaSize.asPortrait());
1210                 } else {
1211                     attributes.setMediaSize(newMediaSize.asLandscape());
1212                 }
1213             }
1214         }
1215 
1216         // Color mode.
1217         final int colorMode = attributes.getColorMode();
1218         if ((capabilities.getColorModes() & colorMode) == 0) {
1219             attributes.setColorMode(defaults.getColorMode());
1220         }
1221 
1222         // Duplex mode.
1223         final int duplexMode = attributes.getDuplexMode();
1224         if ((capabilities.getDuplexModes() & duplexMode) == 0) {
1225             attributes.setDuplexMode(defaults.getDuplexMode());
1226         }
1227 
1228         // Resolution
1229         Resolution resolution = attributes.getResolution();
1230         if (resolution == null || !capabilities.getResolutions().contains(resolution)) {
1231             attributes.setResolution(defaults.getResolution());
1232         }
1233 
1234         // Margins.
1235         if (!Objects.equals(attributes.getMinMargins(), defaults.getMinMargins())) {
1236             clearRanges = true;
1237         }
1238         attributes.setMinMargins(defaults.getMinMargins());
1239 
1240         if (clearRanges) {
1241             clearPageRanges();
1242         }
1243     }
1244 
updateDocument(boolean clearLastError)1245     private boolean updateDocument(boolean clearLastError) {
1246         if (!clearLastError && mPrintedDocument.hasUpdateError()) {
1247             return false;
1248         }
1249 
1250         if (clearLastError && mPrintedDocument.hasUpdateError()) {
1251             mPrintedDocument.clearUpdateError();
1252         }
1253 
1254         final boolean preview = mState != STATE_PRINT_CONFIRMED;
1255         final PageRange[] pages;
1256         if (preview) {
1257             pages = mPrintPreviewController.getRequestedPages();
1258         } else {
1259             pages = mPrintPreviewController.getSelectedPages();
1260         }
1261 
1262         final boolean willUpdate = mPrintedDocument.update(mPrintJob.getAttributes(),
1263                 pages, preview);
1264         updateOptionsUi();
1265 
1266         if (willUpdate && !mPrintedDocument.hasLaidOutPages()) {
1267             // When the update is done we update the print preview.
1268             mProgressMessageController.post();
1269             return true;
1270         } else if (!willUpdate) {
1271             // Update preview.
1272             updatePrintPreviewController(false);
1273         }
1274 
1275         return false;
1276     }
1277 
addCurrentPrinterToHistory()1278     private void addCurrentPrinterToHistory() {
1279         if (mCurrentPrinter != null) {
1280             PrinterId fakePdfPrinterId = mDestinationSpinnerAdapter.getPdfPrinter().getId();
1281             if (!mCurrentPrinter.getId().equals(fakePdfPrinterId)) {
1282                 mPrinterRegistry.addHistoricalPrinter(mCurrentPrinter);
1283             }
1284         }
1285     }
1286 
cancelPrint()1287     private void cancelPrint() {
1288         setState(STATE_PRINT_CANCELED);
1289         mPrintedDocument.cancel(true);
1290         doFinish();
1291     }
1292 
1293     /**
1294      * Update the selected pages from the text field.
1295      */
updateSelectedPagesFromTextField()1296     private void updateSelectedPagesFromTextField() {
1297         PageRange[] selectedPages = computeSelectedPages();
1298         if (!Arrays.equals(mSelectedPages, selectedPages)) {
1299             mSelectedPages = selectedPages;
1300             // Update preview.
1301             updatePrintPreviewController(false);
1302         }
1303     }
1304 
confirmPrint()1305     private void confirmPrint() {
1306         setState(STATE_PRINT_CONFIRMED);
1307 
1308         addCurrentPrinterToHistory();
1309         setUserPrinted();
1310 
1311         // updateSelectedPagesFromTextField migth update the preview, hence apply the preview first
1312         updateSelectedPagesFromPreview();
1313         updateSelectedPagesFromTextField();
1314 
1315         mPrintPreviewController.closeOptions();
1316 
1317         if (canUpdateDocument()) {
1318             updateDocument(false);
1319         }
1320 
1321         if (!mPrintedDocument.isUpdating()) {
1322             requestCreatePdfFileOrFinish();
1323         }
1324     }
1325 
bindUi()1326     private void bindUi() {
1327         // Summary
1328         mSummaryContainer = findViewById(R.id.summary_content);
1329         mSummaryCopies = findViewById(R.id.copies_count_summary);
1330         mSummaryPaperSize = findViewById(R.id.paper_size_summary);
1331 
1332         // Options container
1333         mOptionsContent = findViewById(R.id.options_content);
1334         mOptionsContent.setOptionsStateChangeListener(this);
1335         mOptionsContent.setOpenOptionsController(this);
1336 
1337         OnItemSelectedListener itemSelectedListener = new MyOnItemSelectedListener();
1338         OnClickListener clickListener = new MyClickListener();
1339 
1340         // Copies
1341         mCopiesEditText = findViewById(R.id.copies_edittext);
1342         mCopiesEditText.setOnFocusChangeListener(mSelectAllOnFocusListener);
1343         mCopiesEditText.setText(MIN_COPIES_STRING);
1344         mCopiesEditText.setSelection(mCopiesEditText.getText().length());
1345         mCopiesEditText.addTextChangedListener(new EditTextWatcher());
1346 
1347         // Destination.
1348         mPrintersObserver = new PrintersObserver();
1349         mDestinationSpinnerAdapter.registerDataSetObserver(mPrintersObserver);
1350         mDestinationSpinner = findViewById(R.id.destination_spinner);
1351         mDestinationSpinner.setAdapter(mDestinationSpinnerAdapter);
1352         mDestinationSpinner.setOnItemSelectedListener(itemSelectedListener);
1353 
1354         // Media size.
1355         mMediaSizeSpinnerAdapter = new ArrayAdapter<>(
1356                 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1357         mMediaSizeSpinner = findViewById(R.id.paper_size_spinner);
1358         mMediaSizeSpinner.setAdapter(mMediaSizeSpinnerAdapter);
1359         mMediaSizeSpinner.setOnItemSelectedListener(itemSelectedListener);
1360 
1361         // Color mode.
1362         mColorModeSpinnerAdapter = new ArrayAdapter<>(
1363                 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1364         mColorModeSpinner = findViewById(R.id.color_spinner);
1365         mColorModeSpinner.setAdapter(mColorModeSpinnerAdapter);
1366         mColorModeSpinner.setOnItemSelectedListener(itemSelectedListener);
1367 
1368         // Duplex mode.
1369         mDuplexModeSpinnerAdapter = new ArrayAdapter<>(
1370                 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1371         mDuplexModeSpinner = findViewById(R.id.duplex_spinner);
1372         mDuplexModeSpinner.setAdapter(mDuplexModeSpinnerAdapter);
1373         mDuplexModeSpinner.setOnItemSelectedListener(itemSelectedListener);
1374 
1375         // Orientation
1376         mOrientationSpinnerAdapter = new ArrayAdapter<>(
1377                 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1378         String[] orientationLabels = getResources().getStringArray(
1379                 R.array.orientation_labels);
1380         mOrientationSpinnerAdapter.add(new SpinnerItem<>(
1381                 ORIENTATION_PORTRAIT, orientationLabels[0]));
1382         mOrientationSpinnerAdapter.add(new SpinnerItem<>(
1383                 ORIENTATION_LANDSCAPE, orientationLabels[1]));
1384         mOrientationSpinner = findViewById(R.id.orientation_spinner);
1385         mOrientationSpinner.setAdapter(mOrientationSpinnerAdapter);
1386         mOrientationSpinner.setOnItemSelectedListener(itemSelectedListener);
1387 
1388         // Range options
1389         ArrayAdapter<SpinnerItem<Integer>> rangeOptionsSpinnerAdapter = new ArrayAdapter<>(
1390                 this, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
1391         mRangeOptionsSpinner = findViewById(R.id.range_options_spinner);
1392         mRangeOptionsSpinner.setAdapter(rangeOptionsSpinnerAdapter);
1393         mRangeOptionsSpinner.setOnItemSelectedListener(itemSelectedListener);
1394         updatePageRangeOptions(PrintDocumentInfo.PAGE_COUNT_UNKNOWN);
1395 
1396         // Page range
1397         mPageRangeTitle = findViewById(R.id.page_range_title);
1398         mPageRangeEditText = findViewById(R.id.page_range_edittext);
1399         mPageRangeEditText.setVisibility(View.GONE);
1400         mPageRangeTitle.setVisibility(View.GONE);
1401         mPageRangeEditText.setOnFocusChangeListener(mSelectAllOnFocusListener);
1402         mPageRangeEditText.addTextChangedListener(new RangeTextWatcher());
1403 
1404         // Advanced options button.
1405         mMoreOptionsButton = findViewById(R.id.more_options_button);
1406         mMoreOptionsButton.setOnClickListener(clickListener);
1407 
1408         // Print button
1409         mPrintButton = findViewById(R.id.print_button);
1410         mPrintButton.setOnClickListener(clickListener);
1411 
1412         // The UI is now initialized
1413         mIsOptionsUiBound = true;
1414 
1415         // Special prompt instead of destination spinner for the first time the user printed
1416         if (!hasUserEverPrinted()) {
1417             mShowDestinationPrompt = true;
1418 
1419             mSummaryCopies.setEnabled(false);
1420             mSummaryPaperSize.setEnabled(false);
1421 
1422             mDestinationSpinner.setPerformClickListener((v) -> {
1423                 mShowDestinationPrompt = false;
1424                 mSummaryCopies.setEnabled(true);
1425                 mSummaryPaperSize.setEnabled(true);
1426                 updateOptionsUi();
1427 
1428                 mDestinationSpinner.setPerformClickListener(null);
1429                 mDestinationSpinnerAdapter.notifyDataSetChanged();
1430             });
1431         }
1432     }
1433 
1434     @Override
onCreateLoader(int id, Bundle args)1435     public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) {
1436         return new PrintServicesLoader((PrintManager) getSystemService(Context.PRINT_SERVICE), this,
1437                 PrintManager.ENABLED_SERVICES);
1438     }
1439 
1440     @Override
onLoadFinished(Loader<List<PrintServiceInfo>> loader, List<PrintServiceInfo> services)1441     public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
1442             List<PrintServiceInfo> services) {
1443         ComponentName newAdvancedPrintOptionsActivity = null;
1444         if (mCurrentPrinter != null && services != null) {
1445             final int numServices = services.size();
1446             for (int i = 0; i < numServices; i++) {
1447                 PrintServiceInfo service = services.get(i);
1448 
1449                 if (service.getComponentName().equals(mCurrentPrinter.getId().getServiceName())) {
1450                     String advancedOptionsActivityName = service.getAdvancedOptionsActivityName();
1451 
1452                     if (!TextUtils.isEmpty(advancedOptionsActivityName)) {
1453                         newAdvancedPrintOptionsActivity = new ComponentName(
1454                                 service.getComponentName().getPackageName(),
1455                                 advancedOptionsActivityName);
1456 
1457                         break;
1458                     }
1459                 }
1460             }
1461         }
1462 
1463         if (!Objects.equals(newAdvancedPrintOptionsActivity, mAdvancedPrintOptionsActivity)) {
1464             mAdvancedPrintOptionsActivity = newAdvancedPrintOptionsActivity;
1465             updateOptionsUi();
1466         }
1467 
1468         boolean newArePrintServicesEnabled = services != null && !services.isEmpty();
1469         if (mArePrintServicesEnabled != newArePrintServicesEnabled) {
1470             mArePrintServicesEnabled = newArePrintServicesEnabled;
1471 
1472             // Reload mDestinationSpinnerAdapter as mArePrintServicesEnabled changed and the adapter
1473             // reads that in DestinationAdapter#getMoreItemTitle
1474             if (mDestinationSpinnerAdapter != null) {
1475                 mDestinationSpinnerAdapter.notifyDataSetChanged();
1476             }
1477         }
1478     }
1479 
1480     @Override
onLoaderReset(Loader<List<PrintServiceInfo>> loader)1481     public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) {
1482         if (!(isFinishing() || isDestroyed())) {
1483             onLoadFinished(loader, null);
1484         }
1485     }
1486 
1487     /**
1488      * A dialog that asks the user to approve a {@link PrintService}. This dialog is automatically
1489      * dismissed if the same {@link PrintService} gets approved by another
1490      * {@link PrintServiceApprovalDialog}.
1491      */
1492     public static final class PrintServiceApprovalDialog extends DialogFragment
1493             implements OnSharedPreferenceChangeListener {
1494         private static final String PRINTSERVICE_KEY = "PRINTSERVICE";
1495         private ApprovedPrintServices mApprovedServices;
1496 
1497         /**
1498          * Create a new {@link PrintServiceApprovalDialog} that ask the user to approve a
1499          * {@link PrintService}.
1500          *
1501          * @param printService The {@link ComponentName} of the service to approve
1502          * @return A new {@link PrintServiceApprovalDialog} that might approve the service
1503          */
newInstance(ComponentName printService)1504         static PrintServiceApprovalDialog newInstance(ComponentName printService) {
1505             PrintServiceApprovalDialog dialog = new PrintServiceApprovalDialog();
1506 
1507             Bundle args = new Bundle();
1508             args.putParcelable(PRINTSERVICE_KEY, printService);
1509             dialog.setArguments(args);
1510 
1511             return dialog;
1512         }
1513 
1514         @Override
onStop()1515         public void onStop() {
1516             super.onStop();
1517 
1518             mApprovedServices.unregisterChangeListener(this);
1519         }
1520 
1521         @Override
onStart()1522         public void onStart() {
1523             super.onStart();
1524 
1525             ComponentName printService = getArguments().getParcelable(PRINTSERVICE_KEY);
1526             synchronized (ApprovedPrintServices.sLock) {
1527                 if (mApprovedServices.isApprovedService(printService)) {
1528                     dismiss();
1529                 } else {
1530                     mApprovedServices.registerChangeListenerLocked(this);
1531                 }
1532             }
1533         }
1534 
1535         @Override
onCreateDialog(Bundle savedInstanceState)1536         public Dialog onCreateDialog(Bundle savedInstanceState) {
1537             super.onCreateDialog(savedInstanceState);
1538 
1539             mApprovedServices = new ApprovedPrintServices(getActivity());
1540 
1541             PackageManager packageManager = getActivity().getPackageManager();
1542             CharSequence serviceLabel;
1543             try {
1544                 ComponentName printService = getArguments().getParcelable(PRINTSERVICE_KEY);
1545 
1546                 serviceLabel = packageManager.getApplicationInfo(printService.getPackageName(), 0)
1547                         .loadLabel(packageManager);
1548             } catch (NameNotFoundException e) {
1549                 serviceLabel = null;
1550             }
1551 
1552             AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
1553             builder.setTitle(getString(R.string.print_service_security_warning_title,
1554                     serviceLabel))
1555                     .setMessage(getString(R.string.print_service_security_warning_summary,
1556                             serviceLabel))
1557                     .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
1558                         @Override
1559                         public void onClick(DialogInterface dialog, int id) {
1560                             ComponentName printService =
1561                                     getArguments().getParcelable(PRINTSERVICE_KEY);
1562                             // Prevent onSharedPreferenceChanged from getting triggered
1563                             mApprovedServices
1564                                     .unregisterChangeListener(PrintServiceApprovalDialog.this);
1565 
1566                             mApprovedServices.addApprovedService(printService);
1567                             ((PrintActivity) getActivity()).confirmPrint();
1568                         }
1569                     })
1570                     .setNegativeButton(android.R.string.cancel, null);
1571 
1572             return builder.create();
1573         }
1574 
1575         @Override
onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)1576         public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
1577             ComponentName printService = getArguments().getParcelable(PRINTSERVICE_KEY);
1578 
1579             synchronized (ApprovedPrintServices.sLock) {
1580                 if (mApprovedServices.isApprovedService(printService)) {
1581                     dismiss();
1582                 }
1583             }
1584         }
1585     }
1586 
1587     private final class MyClickListener implements OnClickListener {
1588         @Override
onClick(View view)1589         public void onClick(View view) {
1590             if (view == mPrintButton) {
1591                 if (mCurrentPrinter != null) {
1592                     if (mDestinationSpinnerAdapter.getPdfPrinter() == mCurrentPrinter) {
1593                         confirmPrint();
1594                     } else {
1595                         ApprovedPrintServices approvedServices =
1596                                 new ApprovedPrintServices(PrintActivity.this);
1597 
1598                         ComponentName printService = mCurrentPrinter.getId().getServiceName();
1599                         if (approvedServices.isApprovedService(printService)) {
1600                             confirmPrint();
1601                         } else {
1602                             PrintServiceApprovalDialog.newInstance(printService)
1603                                     .show(getFragmentManager(), "approve");
1604                         }
1605                     }
1606                 } else {
1607                     cancelPrint();
1608                 }
1609             } else if (view == mMoreOptionsButton) {
1610                 if (mPageRangeEditText.getError() == null) {
1611                     // The selected pages is only applied once the user leaves the text field. A click
1612                     // on this button, does not count as leaving.
1613                     updateSelectedPagesFromTextField();
1614                 }
1615 
1616                 if (mCurrentPrinter != null) {
1617                     startAdvancedPrintOptionsActivity(mCurrentPrinter);
1618                 }
1619             }
1620         }
1621     }
1622 
canPrint(PrinterInfo printer)1623     private static boolean canPrint(PrinterInfo printer) {
1624         return printer.getCapabilities() != null
1625                 && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
1626     }
1627 
1628     /**
1629      * Disable all options UI elements, beside the {@link #mDestinationSpinner}
1630      *
1631      * @param disableRange If the range selection options should be disabled
1632      */
disableOptionsUi(boolean disableRange)1633     private void disableOptionsUi(boolean disableRange) {
1634         mCopiesEditText.setEnabled(false);
1635         mCopiesEditText.setFocusable(false);
1636         mMediaSizeSpinner.setEnabled(false);
1637         mColorModeSpinner.setEnabled(false);
1638         mDuplexModeSpinner.setEnabled(false);
1639         mOrientationSpinner.setEnabled(false);
1640         mPrintButton.setVisibility(View.GONE);
1641         mMoreOptionsButton.setEnabled(false);
1642 
1643         if (disableRange) {
1644             mRangeOptionsSpinner.setEnabled(false);
1645             mPageRangeEditText.setEnabled(false);
1646         }
1647     }
1648 
updateOptionsUi()1649     void updateOptionsUi() {
1650         if (!mIsOptionsUiBound) {
1651             return;
1652         }
1653 
1654         // Always update the summary.
1655         updateSummary();
1656 
1657         mDestinationSpinner.setEnabled(!isFinalState(mState));
1658 
1659         if (mState == STATE_PRINT_CONFIRMED
1660                 || mState == STATE_PRINT_COMPLETED
1661                 || mState == STATE_PRINT_CANCELED
1662                 || mState == STATE_UPDATE_FAILED
1663                 || mState == STATE_CREATE_FILE_FAILED
1664                 || mState == STATE_PRINTER_UNAVAILABLE
1665                 || mState == STATE_UPDATE_SLOW) {
1666             disableOptionsUi(isFinalState(mState));
1667             return;
1668         }
1669 
1670         // If no current printer, or it has no capabilities, or it is not
1671         // available, we disable all print options except the destination.
1672         if (mCurrentPrinter == null || !canPrint(mCurrentPrinter)) {
1673             disableOptionsUi(false);
1674             return;
1675         }
1676 
1677         PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
1678         PrintAttributes defaultAttributes = capabilities.getDefaults();
1679 
1680         // Destination.
1681         mDestinationSpinner.setEnabled(true);
1682 
1683         // Media size.
1684         mMediaSizeSpinner.setEnabled(true);
1685 
1686         List<MediaSize> mediaSizes = new ArrayList<>(capabilities.getMediaSizes());
1687         // Sort the media sizes based on the current locale.
1688         Collections.sort(mediaSizes, mMediaSizeComparator);
1689 
1690         PrintAttributes attributes = mPrintJob.getAttributes();
1691 
1692         // If the media sizes changed, we update the adapter and the spinner.
1693         boolean mediaSizesChanged = false;
1694         final int mediaSizeCount = mediaSizes.size();
1695         if (mediaSizeCount != mMediaSizeSpinnerAdapter.getCount()) {
1696             mediaSizesChanged = true;
1697         } else {
1698             for (int i = 0; i < mediaSizeCount; i++) {
1699                 if (!mediaSizes.get(i).equals(mMediaSizeSpinnerAdapter.getItem(i).value)) {
1700                     mediaSizesChanged = true;
1701                     break;
1702                 }
1703             }
1704         }
1705         if (mediaSizesChanged) {
1706             // Remember the old media size to try selecting it again.
1707             int oldMediaSizeNewIndex = AdapterView.INVALID_POSITION;
1708             MediaSize oldMediaSize = attributes.getMediaSize();
1709 
1710             // Rebuild the adapter data.
1711             mMediaSizeSpinnerAdapter.clear();
1712             for (int i = 0; i < mediaSizeCount; i++) {
1713                 MediaSize mediaSize = mediaSizes.get(i);
1714                 if (oldMediaSize != null
1715                         && mediaSize.asPortrait().equals(oldMediaSize.asPortrait())) {
1716                     // Update the index of the old selection.
1717                     oldMediaSizeNewIndex = i;
1718                 }
1719                 mMediaSizeSpinnerAdapter.add(new SpinnerItem<>(
1720                         mediaSize, mediaSize.getLabel(getPackageManager())));
1721             }
1722 
1723             if (oldMediaSizeNewIndex != AdapterView.INVALID_POSITION) {
1724                 // Select the old media size - nothing really changed.
1725                 if (mMediaSizeSpinner.getSelectedItemPosition() != oldMediaSizeNewIndex) {
1726                     mMediaSizeSpinner.setSelection(oldMediaSizeNewIndex);
1727                 }
1728             } else {
1729                 // Select the first or the default.
1730                 final int mediaSizeIndex = Math.max(mediaSizes.indexOf(
1731                         defaultAttributes.getMediaSize()), 0);
1732                 if (mMediaSizeSpinner.getSelectedItemPosition() != mediaSizeIndex) {
1733                     mMediaSizeSpinner.setSelection(mediaSizeIndex);
1734                 }
1735                 // Respect the orientation of the old selection.
1736                 if (oldMediaSize != null) {
1737                     if (oldMediaSize.isPortrait()) {
1738                         attributes.setMediaSize(mMediaSizeSpinnerAdapter
1739                                 .getItem(mediaSizeIndex).value.asPortrait());
1740                     } else {
1741                         attributes.setMediaSize(mMediaSizeSpinnerAdapter
1742                                 .getItem(mediaSizeIndex).value.asLandscape());
1743                     }
1744                 }
1745             }
1746         }
1747 
1748         // Color mode.
1749         mColorModeSpinner.setEnabled(true);
1750         final int colorModes = capabilities.getColorModes();
1751 
1752         // If the color modes changed, we update the adapter and the spinner.
1753         boolean colorModesChanged = false;
1754         if (Integer.bitCount(colorModes) != mColorModeSpinnerAdapter.getCount()) {
1755             colorModesChanged = true;
1756         } else {
1757             int remainingColorModes = colorModes;
1758             int adapterIndex = 0;
1759             while (remainingColorModes != 0) {
1760                 final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
1761                 final int colorMode = 1 << colorBitOffset;
1762                 remainingColorModes &= ~colorMode;
1763                 if (colorMode != mColorModeSpinnerAdapter.getItem(adapterIndex).value) {
1764                     colorModesChanged = true;
1765                     break;
1766                 }
1767                 adapterIndex++;
1768             }
1769         }
1770         if (colorModesChanged) {
1771             // Remember the old color mode to try selecting it again.
1772             int oldColorModeNewIndex = AdapterView.INVALID_POSITION;
1773             final int oldColorMode = attributes.getColorMode();
1774 
1775             // Rebuild the adapter data.
1776             mColorModeSpinnerAdapter.clear();
1777             String[] colorModeLabels = getResources().getStringArray(R.array.color_mode_labels);
1778             int remainingColorModes = colorModes;
1779             while (remainingColorModes != 0) {
1780                 final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
1781                 final int colorMode = 1 << colorBitOffset;
1782                 if (colorMode == oldColorMode) {
1783                     // Update the index of the old selection.
1784                     oldColorModeNewIndex = mColorModeSpinnerAdapter.getCount();
1785                 }
1786                 remainingColorModes &= ~colorMode;
1787                 mColorModeSpinnerAdapter.add(new SpinnerItem<>(colorMode,
1788                         colorModeLabels[colorBitOffset]));
1789             }
1790             if (oldColorModeNewIndex != AdapterView.INVALID_POSITION) {
1791                 // Select the old color mode - nothing really changed.
1792                 if (mColorModeSpinner.getSelectedItemPosition() != oldColorModeNewIndex) {
1793                     mColorModeSpinner.setSelection(oldColorModeNewIndex);
1794                 }
1795             } else {
1796                 // Select the default.
1797                 final int selectedColorMode = colorModes & defaultAttributes.getColorMode();
1798                 final int itemCount = mColorModeSpinnerAdapter.getCount();
1799                 for (int i = 0; i < itemCount; i++) {
1800                     SpinnerItem<Integer> item = mColorModeSpinnerAdapter.getItem(i);
1801                     if (selectedColorMode == item.value) {
1802                         if (mColorModeSpinner.getSelectedItemPosition() != i) {
1803                             mColorModeSpinner.setSelection(i);
1804                         }
1805                         attributes.setColorMode(selectedColorMode);
1806                         break;
1807                     }
1808                 }
1809             }
1810         }
1811 
1812         // Duplex mode.
1813         mDuplexModeSpinner.setEnabled(true);
1814         final int duplexModes = capabilities.getDuplexModes();
1815 
1816         // If the duplex modes changed, we update the adapter and the spinner.
1817         // Note that we use bit count +1 to account for the no duplex option.
1818         boolean duplexModesChanged = false;
1819         if (Integer.bitCount(duplexModes) != mDuplexModeSpinnerAdapter.getCount()) {
1820             duplexModesChanged = true;
1821         } else {
1822             int remainingDuplexModes = duplexModes;
1823             int adapterIndex = 0;
1824             while (remainingDuplexModes != 0) {
1825                 final int duplexBitOffset = Integer.numberOfTrailingZeros(remainingDuplexModes);
1826                 final int duplexMode = 1 << duplexBitOffset;
1827                 remainingDuplexModes &= ~duplexMode;
1828                 if (duplexMode != mDuplexModeSpinnerAdapter.getItem(adapterIndex).value) {
1829                     duplexModesChanged = true;
1830                     break;
1831                 }
1832                 adapterIndex++;
1833             }
1834         }
1835         if (duplexModesChanged) {
1836             // Remember the old duplex mode to try selecting it again. Also the fallback
1837             // is no duplexing which is always the first item in the dropdown.
1838             int oldDuplexModeNewIndex = AdapterView.INVALID_POSITION;
1839             final int oldDuplexMode = attributes.getDuplexMode();
1840 
1841             // Rebuild the adapter data.
1842             mDuplexModeSpinnerAdapter.clear();
1843             String[] duplexModeLabels = getResources().getStringArray(R.array.duplex_mode_labels);
1844             int remainingDuplexModes = duplexModes;
1845             while (remainingDuplexModes != 0) {
1846                 final int duplexBitOffset = Integer.numberOfTrailingZeros(remainingDuplexModes);
1847                 final int duplexMode = 1 << duplexBitOffset;
1848                 if (duplexMode == oldDuplexMode) {
1849                     // Update the index of the old selection.
1850                     oldDuplexModeNewIndex = mDuplexModeSpinnerAdapter.getCount();
1851                 }
1852                 remainingDuplexModes &= ~duplexMode;
1853                 mDuplexModeSpinnerAdapter.add(new SpinnerItem<>(duplexMode,
1854                         duplexModeLabels[duplexBitOffset]));
1855             }
1856 
1857             if (oldDuplexModeNewIndex != AdapterView.INVALID_POSITION) {
1858                 // Select the old duplex mode - nothing really changed.
1859                 if (mDuplexModeSpinner.getSelectedItemPosition() != oldDuplexModeNewIndex) {
1860                     mDuplexModeSpinner.setSelection(oldDuplexModeNewIndex);
1861                 }
1862             } else {
1863                 // Select the default.
1864                 final int selectedDuplexMode = defaultAttributes.getDuplexMode();
1865                 final int itemCount = mDuplexModeSpinnerAdapter.getCount();
1866                 for (int i = 0; i < itemCount; i++) {
1867                     SpinnerItem<Integer> item = mDuplexModeSpinnerAdapter.getItem(i);
1868                     if (selectedDuplexMode == item.value) {
1869                         if (mDuplexModeSpinner.getSelectedItemPosition() != i) {
1870                             mDuplexModeSpinner.setSelection(i);
1871                         }
1872                         attributes.setDuplexMode(selectedDuplexMode);
1873                         break;
1874                     }
1875                 }
1876             }
1877         }
1878 
1879         mDuplexModeSpinner.setEnabled(mDuplexModeSpinnerAdapter.getCount() > 1);
1880 
1881         // Orientation
1882         mOrientationSpinner.setEnabled(true);
1883         MediaSize mediaSize = attributes.getMediaSize();
1884         if (mediaSize != null) {
1885             if (mediaSize.isPortrait()
1886                     && mOrientationSpinner.getSelectedItemPosition() != 0) {
1887                 mOrientationSpinner.setSelection(0);
1888             } else if (!mediaSize.isPortrait()
1889                     && mOrientationSpinner.getSelectedItemPosition() != 1) {
1890                 mOrientationSpinner.setSelection(1);
1891             }
1892         }
1893 
1894         // Range options
1895         PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
1896         final int pageCount = getAdjustedPageCount(info);
1897         if (pageCount > 0) {
1898             if (info != null) {
1899                 if (pageCount == 1) {
1900                     mRangeOptionsSpinner.setEnabled(false);
1901                 } else {
1902                     mRangeOptionsSpinner.setEnabled(true);
1903                     if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
1904                         if (!mPageRangeEditText.isEnabled()) {
1905                             mPageRangeEditText.setEnabled(true);
1906                             mPageRangeEditText.setVisibility(View.VISIBLE);
1907                             mPageRangeTitle.setVisibility(View.VISIBLE);
1908                             mPageRangeEditText.requestFocus();
1909                             InputMethodManager imm = (InputMethodManager)
1910                                     getSystemService(Context.INPUT_METHOD_SERVICE);
1911                             imm.showSoftInput(mPageRangeEditText, 0);
1912                         }
1913                     } else {
1914                         mPageRangeEditText.setEnabled(false);
1915                         mPageRangeEditText.setVisibility(View.GONE);
1916                         mPageRangeTitle.setVisibility(View.GONE);
1917                     }
1918                 }
1919             } else {
1920                 if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
1921                     mRangeOptionsSpinner.setSelection(0);
1922                     mPageRangeEditText.setText("");
1923                 }
1924                 mRangeOptionsSpinner.setEnabled(false);
1925                 mPageRangeEditText.setEnabled(false);
1926                 mPageRangeEditText.setVisibility(View.GONE);
1927                 mPageRangeTitle.setVisibility(View.GONE);
1928             }
1929         }
1930 
1931         final int newPageCount = getAdjustedPageCount(info);
1932         if (newPageCount != mCurrentPageCount) {
1933             mCurrentPageCount = newPageCount;
1934             updatePageRangeOptions(newPageCount);
1935         }
1936 
1937         // Advanced print options
1938         if (mAdvancedPrintOptionsActivity != null) {
1939             mMoreOptionsButton.setVisibility(View.VISIBLE);
1940 
1941             mMoreOptionsButton.setEnabled(!mIsMoreOptionsActivityInProgress);
1942         } else {
1943             mMoreOptionsButton.setVisibility(View.GONE);
1944             mMoreOptionsButton.setEnabled(false);
1945         }
1946 
1947         // Print
1948         if (mDestinationSpinnerAdapter.getPdfPrinter() != mCurrentPrinter) {
1949             mPrintButton.setImageResource(com.android.internal.R.drawable.ic_print);
1950             mPrintButton.setContentDescription(getString(R.string.print_button));
1951         } else {
1952             mPrintButton.setImageResource(R.drawable.ic_menu_savetopdf);
1953             mPrintButton.setContentDescription(getString(R.string.savetopdf_button));
1954         }
1955         if (!mPrintedDocument.getDocumentInfo().updated
1956                 ||(mRangeOptionsSpinner.getSelectedItemPosition() == 1
1957                 && (TextUtils.isEmpty(mPageRangeEditText.getText()) || hasErrors()))
1958                 || (mRangeOptionsSpinner.getSelectedItemPosition() == 0
1959                 && (mPrintedDocument.getDocumentInfo() == null || hasErrors()))) {
1960             mPrintButton.setVisibility(View.GONE);
1961         } else {
1962             mPrintButton.setVisibility(View.VISIBLE);
1963         }
1964 
1965         // Copies
1966         if (mDestinationSpinnerAdapter.getPdfPrinter() != mCurrentPrinter) {
1967             mCopiesEditText.setEnabled(true);
1968             mCopiesEditText.setFocusableInTouchMode(true);
1969         } else {
1970             CharSequence text = mCopiesEditText.getText();
1971             if (TextUtils.isEmpty(text) || !MIN_COPIES_STRING.equals(text.toString())) {
1972                 mCopiesEditText.setText(MIN_COPIES_STRING);
1973             }
1974             mCopiesEditText.setEnabled(false);
1975             mCopiesEditText.setFocusable(false);
1976         }
1977         if (mCopiesEditText.getError() == null
1978                 && TextUtils.isEmpty(mCopiesEditText.getText())) {
1979             mCopiesEditText.setText(MIN_COPIES_STRING);
1980             mCopiesEditText.requestFocus();
1981         }
1982 
1983         if (mShowDestinationPrompt) {
1984             disableOptionsUi(false);
1985         }
1986     }
1987 
updateSummary()1988     private void updateSummary() {
1989         if (!mIsOptionsUiBound) {
1990             return;
1991         }
1992 
1993         CharSequence copiesText = null;
1994         CharSequence mediaSizeText = null;
1995 
1996         if (!TextUtils.isEmpty(mCopiesEditText.getText())) {
1997             copiesText = mCopiesEditText.getText();
1998             mSummaryCopies.setText(copiesText);
1999         }
2000 
2001         final int selectedMediaIndex = mMediaSizeSpinner.getSelectedItemPosition();
2002         if (selectedMediaIndex >= 0) {
2003             SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(selectedMediaIndex);
2004             mediaSizeText = mediaItem.label;
2005             mSummaryPaperSize.setText(mediaSizeText);
2006         }
2007 
2008         if (!TextUtils.isEmpty(copiesText) && !TextUtils.isEmpty(mediaSizeText)) {
2009             String summaryText = getString(R.string.summary_template, copiesText, mediaSizeText);
2010             mSummaryContainer.setContentDescription(summaryText);
2011         }
2012     }
2013 
updatePageRangeOptions(int pageCount)2014     private void updatePageRangeOptions(int pageCount) {
2015         @SuppressWarnings("unchecked")
2016         ArrayAdapter<SpinnerItem<Integer>> rangeOptionsSpinnerAdapter =
2017                 (ArrayAdapter<SpinnerItem<Integer>>) mRangeOptionsSpinner.getAdapter();
2018         rangeOptionsSpinnerAdapter.clear();
2019 
2020         final int[] rangeOptionsValues = getResources().getIntArray(
2021                 R.array.page_options_values);
2022 
2023         String pageCountLabel = (pageCount > 0) ? String.valueOf(pageCount) : "";
2024         String[] rangeOptionsLabels = new String[] {
2025             getString(R.string.template_all_pages, pageCountLabel),
2026             getString(R.string.template_page_range, pageCountLabel)
2027         };
2028 
2029         final int rangeOptionsCount = rangeOptionsLabels.length;
2030         for (int i = 0; i < rangeOptionsCount; i++) {
2031             rangeOptionsSpinnerAdapter.add(new SpinnerItem<>(
2032                     rangeOptionsValues[i], rangeOptionsLabels[i]));
2033         }
2034     }
2035 
computeSelectedPages()2036     private PageRange[] computeSelectedPages() {
2037         if (hasErrors()) {
2038             return null;
2039         }
2040 
2041         if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
2042             PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
2043             final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
2044 
2045             return PageRangeUtils.parsePageRanges(mPageRangeEditText.getText(), pageCount);
2046         }
2047 
2048         return PageRange.ALL_PAGES_ARRAY;
2049     }
2050 
getAdjustedPageCount(PrintDocumentInfo info)2051     private int getAdjustedPageCount(PrintDocumentInfo info) {
2052         if (info != null) {
2053             final int pageCount = info.getPageCount();
2054             if (pageCount != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) {
2055                 return pageCount;
2056             }
2057         }
2058         // If the app does not tell us how many pages are in the
2059         // doc we ask for all pages and use the document page count.
2060         return mPrintPreviewController.getFilePageCount();
2061     }
2062 
hasErrors()2063     private boolean hasErrors() {
2064         return (mCopiesEditText.getError() != null)
2065                 || (mPageRangeEditText.getVisibility() == View.VISIBLE
2066                 && mPageRangeEditText.getError() != null);
2067     }
2068 
onPrinterAvailable(PrinterInfo printer)2069     public void onPrinterAvailable(PrinterInfo printer) {
2070         if (mCurrentPrinter != null && mCurrentPrinter.equals(printer)) {
2071             setState(STATE_CONFIGURING);
2072             if (canUpdateDocument()) {
2073                 updateDocument(false);
2074             }
2075             ensurePreviewUiShown();
2076         }
2077     }
2078 
onPrinterUnavailable(PrinterInfo printer)2079     public void onPrinterUnavailable(PrinterInfo printer) {
2080         if (mCurrentPrinter == null || mCurrentPrinter.getId().equals(printer.getId())) {
2081             setState(STATE_PRINTER_UNAVAILABLE);
2082             mPrintedDocument.cancel(false);
2083             ensureErrorUiShown(getString(R.string.print_error_printer_unavailable),
2084                     PrintErrorFragment.ACTION_NONE);
2085         }
2086     }
2087 
canUpdateDocument()2088     private boolean canUpdateDocument() {
2089         if (mPrintedDocument.isDestroyed()) {
2090             return false;
2091         }
2092 
2093         if (hasErrors()) {
2094             return false;
2095         }
2096 
2097         PrintAttributes attributes = mPrintJob.getAttributes();
2098 
2099         final int colorMode = attributes.getColorMode();
2100         if (colorMode != PrintAttributes.COLOR_MODE_COLOR
2101                 && colorMode != PrintAttributes.COLOR_MODE_MONOCHROME) {
2102             return false;
2103         }
2104         if (attributes.getMediaSize() == null) {
2105             return false;
2106         }
2107         if (attributes.getMinMargins() == null) {
2108             return false;
2109         }
2110         if (attributes.getResolution() == null) {
2111             return false;
2112         }
2113 
2114         if (mCurrentPrinter == null) {
2115             return false;
2116         }
2117         PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities();
2118         if (capabilities == null) {
2119             return false;
2120         }
2121         if (mCurrentPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE) {
2122             return false;
2123         }
2124 
2125         return true;
2126     }
2127 
transformDocumentAndFinish(final Uri writeToUri)2128     private void transformDocumentAndFinish(final Uri writeToUri) {
2129         // If saving to PDF, apply the attibutes as we are acting as a print service.
2130         PrintAttributes attributes = mDestinationSpinnerAdapter.getPdfPrinter() == mCurrentPrinter
2131                 ?  mPrintJob.getAttributes() : null;
2132         new DocumentTransformer(this, mPrintJob, mFileProvider, attributes, error -> {
2133             if (error == null) {
2134                 if (writeToUri != null) {
2135                     mPrintedDocument.writeContent(getContentResolver(), writeToUri);
2136                 }
2137                 setState(STATE_PRINT_COMPLETED);
2138                 doFinish();
2139             } else {
2140                 onPrintDocumentError(error);
2141             }
2142         }).transform();
2143     }
2144 
doFinish()2145     private void doFinish() {
2146         if (mPrintedDocument != null && mPrintedDocument.isUpdating()) {
2147             // The printedDocument will call doFinish() when the current command finishes
2148             return;
2149         }
2150 
2151         if (mIsFinishing) {
2152             return;
2153         }
2154 
2155         mIsFinishing = true;
2156 
2157         if (mPrinterRegistry != null) {
2158             mPrinterRegistry.setTrackedPrinter(null);
2159             mPrinterRegistry.setOnPrintersChangeListener(null);
2160         }
2161 
2162         if (mPrintersObserver != null) {
2163             mDestinationSpinnerAdapter.unregisterDataSetObserver(mPrintersObserver);
2164         }
2165 
2166         if (mSpoolerProvider != null) {
2167             mSpoolerProvider.destroy();
2168         }
2169 
2170         if (mProgressMessageController != null) {
2171             setState(mProgressMessageController.cancel());
2172         }
2173 
2174         if (mState != STATE_INITIALIZING) {
2175             mPrintedDocument.finish();
2176             mPrintedDocument.destroy();
2177             mPrintPreviewController.destroy(new Runnable() {
2178                 @Override
2179                 public void run() {
2180                     finish();
2181                 }
2182             });
2183         } else {
2184             finish();
2185         }
2186     }
2187 
2188     private final class SpinnerItem<T> {
2189         final T value;
2190         final CharSequence label;
2191 
SpinnerItem(T value, CharSequence label)2192         public SpinnerItem(T value, CharSequence label) {
2193             this.value = value;
2194             this.label = label;
2195         }
2196 
2197         @Override
toString()2198         public String toString() {
2199             return label.toString();
2200         }
2201     }
2202 
2203     private final class PrinterAvailabilityDetector implements Runnable {
2204         private static final long UNAVAILABLE_TIMEOUT_MILLIS = 10000; // 10sec
2205 
2206         private boolean mPosted;
2207 
2208         private boolean mPrinterUnavailable;
2209 
2210         private PrinterInfo mPrinter;
2211 
updatePrinter(PrinterInfo printer)2212         public void updatePrinter(PrinterInfo printer) {
2213             if (printer.equals(mDestinationSpinnerAdapter.getPdfPrinter())) {
2214                 return;
2215             }
2216 
2217             final boolean available = printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE
2218                     && printer.getCapabilities() != null;
2219             final boolean notifyIfAvailable;
2220 
2221             if (mPrinter == null || !mPrinter.getId().equals(printer.getId())) {
2222                 notifyIfAvailable = true;
2223                 unpostIfNeeded();
2224                 mPrinterUnavailable = false;
2225                 mPrinter = new PrinterInfo.Builder(printer).build();
2226             } else {
2227                 notifyIfAvailable =
2228                         (mPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE
2229                                 && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE)
2230                                 || (mPrinter.getCapabilities() == null
2231                                 && printer.getCapabilities() != null);
2232                 mPrinter = printer;
2233             }
2234 
2235             if (available) {
2236                 unpostIfNeeded();
2237                 mPrinterUnavailable = false;
2238                 if (notifyIfAvailable) {
2239                     onPrinterAvailable(mPrinter);
2240                 }
2241             } else {
2242                 if (!mPrinterUnavailable) {
2243                     postIfNeeded();
2244                 }
2245             }
2246         }
2247 
cancel()2248         public void cancel() {
2249             unpostIfNeeded();
2250             mPrinterUnavailable = false;
2251         }
2252 
postIfNeeded()2253         private void postIfNeeded() {
2254             if (!mPosted) {
2255                 mPosted = true;
2256                 mDestinationSpinner.postDelayed(this, UNAVAILABLE_TIMEOUT_MILLIS);
2257             }
2258         }
2259 
unpostIfNeeded()2260         private void unpostIfNeeded() {
2261             if (mPosted) {
2262                 mPosted = false;
2263                 mDestinationSpinner.removeCallbacks(this);
2264             }
2265         }
2266 
2267         @Override
run()2268         public void run() {
2269             mPosted = false;
2270             mPrinterUnavailable = true;
2271             onPrinterUnavailable(mPrinter);
2272         }
2273     }
2274 
2275     private static final class PrinterHolder {
2276         PrinterInfo printer;
2277         boolean removed;
2278 
PrinterHolder(PrinterInfo printer)2279         public PrinterHolder(PrinterInfo printer) {
2280             this.printer = printer;
2281         }
2282     }
2283 
2284 
2285     /**
2286      * Check if the user has ever printed a document
2287      *
2288      * @return true iff the user has ever printed a document
2289      */
hasUserEverPrinted()2290     private boolean hasUserEverPrinted() {
2291         SharedPreferences preferences = getSharedPreferences(HAS_PRINTED_PREF, MODE_PRIVATE);
2292 
2293         return preferences.getBoolean(HAS_PRINTED_PREF, false);
2294     }
2295 
2296     /**
2297      * Remember that the user printed a document
2298      */
setUserPrinted()2299     private void setUserPrinted() {
2300         SharedPreferences preferences = getSharedPreferences(HAS_PRINTED_PREF, MODE_PRIVATE);
2301 
2302         if (!preferences.getBoolean(HAS_PRINTED_PREF, false)) {
2303             SharedPreferences.Editor edit = preferences.edit();
2304 
2305             edit.putBoolean(HAS_PRINTED_PREF, true);
2306             edit.apply();
2307         }
2308     }
2309 
2310     private final class DestinationAdapter extends BaseAdapter
2311             implements PrinterRegistry.OnPrintersChangeListener {
2312         private final List<PrinterHolder> mPrinterHolders = new ArrayList<>();
2313 
2314         private final PrinterHolder mFakePdfPrinterHolder;
2315 
2316         private boolean mHistoricalPrintersLoaded;
2317 
2318         /**
2319          * Has the {@link #mDestinationSpinner} ever used a view from printer_dropdown_prompt
2320          */
2321         private boolean hadPromptView;
2322 
DestinationAdapter()2323         public DestinationAdapter() {
2324             mHistoricalPrintersLoaded = mPrinterRegistry.areHistoricalPrintersLoaded();
2325             if (mHistoricalPrintersLoaded) {
2326                 addPrinters(mPrinterHolders, mPrinterRegistry.getPrinters());
2327             }
2328             mPrinterRegistry.setOnPrintersChangeListener(this);
2329             mFakePdfPrinterHolder = new PrinterHolder(createFakePdfPrinter());
2330         }
2331 
getPdfPrinter()2332         public PrinterInfo getPdfPrinter() {
2333             return mFakePdfPrinterHolder.printer;
2334         }
2335 
getPrinterIndex(PrinterId printerId)2336         public int getPrinterIndex(PrinterId printerId) {
2337             for (int i = 0; i < getCount(); i++) {
2338                 PrinterHolder printerHolder = (PrinterHolder) getItem(i);
2339                 if (printerHolder != null && printerHolder.printer.getId().equals(printerId)) {
2340                     return i;
2341                 }
2342             }
2343             return AdapterView.INVALID_POSITION;
2344         }
2345 
ensurePrinterInVisibleAdapterPosition(PrinterInfo printer)2346         public void ensurePrinterInVisibleAdapterPosition(PrinterInfo printer) {
2347             final int printerCount = mPrinterHolders.size();
2348             boolean isKnownPrinter = false;
2349             for (int i = 0; i < printerCount; i++) {
2350                 PrinterHolder printerHolder = mPrinterHolders.get(i);
2351 
2352                 if (printerHolder.printer.getId().equals(printer.getId())) {
2353                     isKnownPrinter = true;
2354 
2355                     // If already in the list - do nothing.
2356                     if (i < getCount() - 2) {
2357                         break;
2358                     }
2359                     // Else replace the last one (two items are not printers).
2360                     final int lastPrinterIndex = getCount() - 3;
2361                     mPrinterHolders.set(i, mPrinterHolders.get(lastPrinterIndex));
2362                     mPrinterHolders.set(lastPrinterIndex, printerHolder);
2363                     break;
2364                 }
2365             }
2366 
2367             if (!isKnownPrinter) {
2368                 PrinterHolder printerHolder = new PrinterHolder(printer);
2369                 printerHolder.removed = true;
2370 
2371                 mPrinterHolders.add(Math.max(0, getCount() - 3), printerHolder);
2372             }
2373 
2374             // Force reload to adjust selection in PrintersObserver.onChanged()
2375             notifyDataSetChanged();
2376         }
2377 
2378         @Override
getCount()2379         public int getCount() {
2380             if (mHistoricalPrintersLoaded) {
2381                 return Math.min(mPrinterHolders.size() + 2, DEST_ADAPTER_MAX_ITEM_COUNT);
2382             }
2383             return 0;
2384         }
2385 
2386         @Override
isEnabled(int position)2387         public boolean isEnabled(int position) {
2388             Object item = getItem(position);
2389             if (item instanceof PrinterHolder) {
2390                 PrinterHolder printerHolder = (PrinterHolder) item;
2391                 return !printerHolder.removed
2392                         && printerHolder.printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
2393             }
2394             return true;
2395         }
2396 
2397         @Override
getItem(int position)2398         public Object getItem(int position) {
2399             if (mPrinterHolders.isEmpty()) {
2400                 if (position == 0) {
2401                     return mFakePdfPrinterHolder;
2402                 }
2403             } else {
2404                 if (position < 1) {
2405                     return mPrinterHolders.get(position);
2406                 }
2407                 if (position == 1) {
2408                     return mFakePdfPrinterHolder;
2409                 }
2410                 if (position < getCount() - 1) {
2411                     return mPrinterHolders.get(position - 1);
2412                 }
2413             }
2414             return null;
2415         }
2416 
2417         @Override
getItemId(int position)2418         public long getItemId(int position) {
2419             if (mPrinterHolders.isEmpty()) {
2420                 if (position == 0) {
2421                     return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
2422                 } else if (position == 1) {
2423                     return DEST_ADAPTER_ITEM_ID_MORE;
2424                 }
2425             } else {
2426                 if (position == 1) {
2427                     return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
2428                 }
2429                 if (position == getCount() - 1) {
2430                     return DEST_ADAPTER_ITEM_ID_MORE;
2431                 }
2432             }
2433             return position;
2434         }
2435 
2436         @Override
getDropDownView(int position, View convertView, ViewGroup parent)2437         public View getDropDownView(int position, View convertView, ViewGroup parent) {
2438             View view = getView(position, convertView, parent);
2439             view.setEnabled(isEnabled(position));
2440             return view;
2441         }
2442 
getMoreItemTitle()2443         private String getMoreItemTitle() {
2444             if (mArePrintServicesEnabled) {
2445                 return getString(R.string.all_printers);
2446             } else {
2447                 return getString(R.string.print_add_printer);
2448             }
2449         }
2450 
2451         @Override
getView(int position, View convertView, ViewGroup parent)2452         public View getView(int position, View convertView, ViewGroup parent) {
2453             if (mShowDestinationPrompt) {
2454                 if (convertView == null) {
2455                     convertView = getLayoutInflater().inflate(
2456                             R.layout.printer_dropdown_prompt, parent, false);
2457                     hadPromptView = true;
2458                 }
2459 
2460                 return convertView;
2461             } else {
2462                 // We don't know if we got an recyled printer_dropdown_prompt, hence do not use it
2463                 if (hadPromptView || convertView == null) {
2464                     convertView = getLayoutInflater().inflate(
2465                             R.layout.printer_dropdown_item, parent, false);
2466                 }
2467             }
2468 
2469             CharSequence title = null;
2470             CharSequence subtitle = null;
2471             Drawable icon = null;
2472 
2473             if (mPrinterHolders.isEmpty()) {
2474                 if (position == 0 && getPdfPrinter() != null) {
2475                     PrinterHolder printerHolder = (PrinterHolder) getItem(position);
2476                     title = printerHolder.printer.getName();
2477                     icon = getResources().getDrawable(R.drawable.ic_pdf_printer, null);
2478                 } else if (position == 1) {
2479                     title = getMoreItemTitle();
2480                 }
2481             } else {
2482                 if (position == 1 && getPdfPrinter() != null) {
2483                     PrinterHolder printerHolder = (PrinterHolder) getItem(position);
2484                     title = printerHolder.printer.getName();
2485                     icon = getResources().getDrawable(R.drawable.ic_pdf_printer, null);
2486                 } else if (position == getCount() - 1) {
2487                     title = getMoreItemTitle();
2488                 } else {
2489                     PrinterHolder printerHolder = (PrinterHolder) getItem(position);
2490                     PrinterInfo printInfo = printerHolder.printer;
2491 
2492                     title = printInfo.getName();
2493                     icon = printInfo.loadIcon(PrintActivity.this);
2494                     subtitle = printInfo.getDescription();
2495                 }
2496             }
2497 
2498             TextView titleView = (TextView) convertView.findViewById(R.id.title);
2499             titleView.setText(title);
2500 
2501             TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle);
2502             if (!TextUtils.isEmpty(subtitle)) {
2503                 subtitleView.setText(subtitle);
2504                 subtitleView.setVisibility(View.VISIBLE);
2505             } else {
2506                 subtitleView.setText(null);
2507                 subtitleView.setVisibility(View.GONE);
2508             }
2509 
2510             ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
2511             if (icon != null) {
2512                 iconView.setVisibility(View.VISIBLE);
2513                 if (!isEnabled(position)) {
2514                     icon.mutate();
2515 
2516                     TypedValue value = new TypedValue();
2517                     getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true);
2518                     icon.setAlpha((int)(value.getFloat() * 255));
2519                 }
2520                 iconView.setImageDrawable(icon);
2521             } else {
2522                 iconView.setVisibility(View.INVISIBLE);
2523             }
2524 
2525             return convertView;
2526         }
2527 
2528         @Override
onPrintersChanged(List<PrinterInfo> printers)2529         public void onPrintersChanged(List<PrinterInfo> printers) {
2530             // We rearrange the printers if the user selects a printer
2531             // not shown in the initial short list. Therefore, we have
2532             // to keep the printer order.
2533 
2534             // Check if historical printers are loaded as this adapter is open
2535             // for busyness only if they are. This member is updated here and
2536             // when the adapter is created because the historical printers may
2537             // be loaded before or after the adapter is created.
2538             mHistoricalPrintersLoaded = mPrinterRegistry.areHistoricalPrintersLoaded();
2539 
2540             // No old printers - do not bother keeping their position.
2541             if (mPrinterHolders.isEmpty()) {
2542                 addPrinters(mPrinterHolders, printers);
2543                 notifyDataSetChanged();
2544                 return;
2545             }
2546 
2547             // Add the new printers to a map.
2548             ArrayMap<PrinterId, PrinterInfo> newPrintersMap = new ArrayMap<>();
2549             final int printerCount = printers.size();
2550             for (int i = 0; i < printerCount; i++) {
2551                 PrinterInfo printer = printers.get(i);
2552                 newPrintersMap.put(printer.getId(), printer);
2553             }
2554 
2555             List<PrinterHolder> newPrinterHolders = new ArrayList<>();
2556 
2557             // Update printers we already have which are either updated or removed.
2558             // We do not remove the currently selected printer.
2559             final int oldPrinterCount = mPrinterHolders.size();
2560             for (int i = 0; i < oldPrinterCount; i++) {
2561                 PrinterHolder printerHolder = mPrinterHolders.get(i);
2562                 PrinterId oldPrinterId = printerHolder.printer.getId();
2563                 PrinterInfo updatedPrinter = newPrintersMap.remove(oldPrinterId);
2564 
2565                 if (updatedPrinter != null) {
2566                     printerHolder.printer = updatedPrinter;
2567                     printerHolder.removed = false;
2568                     if (canPrint(printerHolder.printer)) {
2569                         onPrinterAvailable(printerHolder.printer);
2570                     } else {
2571                         onPrinterUnavailable(printerHolder.printer);
2572                     }
2573                     newPrinterHolders.add(printerHolder);
2574                 } else if (mCurrentPrinter != null && mCurrentPrinter.getId().equals(oldPrinterId)){
2575                     printerHolder.removed = true;
2576                     onPrinterUnavailable(printerHolder.printer);
2577                     newPrinterHolders.add(printerHolder);
2578                 }
2579             }
2580 
2581             // Add the rest of the new printers, i.e. what is left.
2582             addPrinters(newPrinterHolders, newPrintersMap.values());
2583 
2584             mPrinterHolders.clear();
2585             mPrinterHolders.addAll(newPrinterHolders);
2586 
2587             notifyDataSetChanged();
2588         }
2589 
2590         @Override
onPrintersInvalid()2591         public void onPrintersInvalid() {
2592             mPrinterHolders.clear();
2593             notifyDataSetInvalidated();
2594         }
2595 
getPrinterHolder(PrinterId printerId)2596         public PrinterHolder getPrinterHolder(PrinterId printerId) {
2597             final int itemCount = getCount();
2598             for (int i = 0; i < itemCount; i++) {
2599                 Object item = getItem(i);
2600                 if (item instanceof PrinterHolder) {
2601                     PrinterHolder printerHolder = (PrinterHolder) item;
2602                     if (printerId.equals(printerHolder.printer.getId())) {
2603                         return printerHolder;
2604                     }
2605                 }
2606             }
2607             return null;
2608         }
2609 
2610         /**
2611          * Remove a printer from the holders if it is marked as removed.
2612          *
2613          * @param printerId the id of the printer to remove.
2614          *
2615          * @return true iff the printer was removed.
2616          */
pruneRemovedPrinter(PrinterId printerId)2617         public boolean pruneRemovedPrinter(PrinterId printerId) {
2618             final int holderCounts = mPrinterHolders.size();
2619             for (int i = holderCounts - 1; i >= 0; i--) {
2620                 PrinterHolder printerHolder = mPrinterHolders.get(i);
2621 
2622                 if (printerHolder.printer.getId().equals(printerId) && printerHolder.removed) {
2623                     mPrinterHolders.remove(i);
2624                     return true;
2625                 }
2626             }
2627 
2628             return false;
2629         }
2630 
addPrinters(List<PrinterHolder> list, Collection<PrinterInfo> printers)2631         private void addPrinters(List<PrinterHolder> list, Collection<PrinterInfo> printers) {
2632             for (PrinterInfo printer : printers) {
2633                 PrinterHolder printerHolder = new PrinterHolder(printer);
2634                 list.add(printerHolder);
2635             }
2636         }
2637 
createFakePdfPrinter()2638         private PrinterInfo createFakePdfPrinter() {
2639             ArraySet<MediaSize> allMediaSizes = MediaSize.getAllPredefinedSizes();
2640             MediaSize defaultMediaSize = MediaSizeUtils.getDefault(PrintActivity.this);
2641 
2642             PrinterId printerId = new PrinterId(getComponentName(), "PDF printer");
2643 
2644             PrinterCapabilitiesInfo.Builder builder =
2645                     new PrinterCapabilitiesInfo.Builder(printerId);
2646 
2647             final int mediaSizeCount = allMediaSizes.size();
2648             for (int i = 0; i < mediaSizeCount; i++) {
2649                 MediaSize mediaSize = allMediaSizes.valueAt(i);
2650                 builder.addMediaSize(mediaSize, mediaSize.equals(defaultMediaSize));
2651             }
2652 
2653             builder.addResolution(new Resolution("PDF resolution", "PDF resolution", 300, 300),
2654                     true);
2655             builder.setColorModes(PrintAttributes.COLOR_MODE_COLOR
2656                     | PrintAttributes.COLOR_MODE_MONOCHROME, PrintAttributes.COLOR_MODE_COLOR);
2657 
2658             return new PrinterInfo.Builder(printerId, getString(R.string.save_as_pdf),
2659                     PrinterInfo.STATUS_IDLE).setCapabilities(builder.build()).build();
2660         }
2661     }
2662 
2663     private final class PrintersObserver extends DataSetObserver {
2664         @Override
onChanged()2665         public void onChanged() {
2666             PrinterInfo oldPrinterState = mCurrentPrinter;
2667             if (oldPrinterState == null) {
2668                 return;
2669             }
2670 
2671             PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
2672                     oldPrinterState.getId());
2673             PrinterInfo newPrinterState = printerHolder.printer;
2674 
2675             if (printerHolder.removed) {
2676                 onPrinterUnavailable(newPrinterState);
2677             }
2678 
2679             if (mDestinationSpinner.getSelectedItem() != printerHolder) {
2680                 mDestinationSpinner.setSelection(
2681                         mDestinationSpinnerAdapter.getPrinterIndex(newPrinterState.getId()));
2682             }
2683 
2684             if (oldPrinterState.equals(newPrinterState)) {
2685                 return;
2686             }
2687 
2688             PrinterCapabilitiesInfo oldCapab = oldPrinterState.getCapabilities();
2689             PrinterCapabilitiesInfo newCapab = newPrinterState.getCapabilities();
2690 
2691             final boolean hadCabab = oldCapab != null;
2692             final boolean hasCapab = newCapab != null;
2693             final boolean gotCapab = oldCapab == null && newCapab != null;
2694             final boolean lostCapab = oldCapab != null && newCapab == null;
2695             final boolean capabChanged = capabilitiesChanged(oldCapab, newCapab);
2696 
2697             final int oldStatus = oldPrinterState.getStatus();
2698             final int newStatus = newPrinterState.getStatus();
2699 
2700             final boolean isActive = newStatus != PrinterInfo.STATUS_UNAVAILABLE;
2701             final boolean becameActive = (oldStatus == PrinterInfo.STATUS_UNAVAILABLE
2702                     && oldStatus != newStatus);
2703             final boolean becameInactive = (newStatus == PrinterInfo.STATUS_UNAVAILABLE
2704                     && oldStatus != newStatus);
2705 
2706             mPrinterAvailabilityDetector.updatePrinter(newPrinterState);
2707 
2708             mCurrentPrinter = newPrinterState;
2709 
2710             final boolean updateNeeded = ((capabChanged && hasCapab && isActive)
2711                     || (becameActive && hasCapab) || (isActive && gotCapab));
2712 
2713             if (capabChanged && hasCapab) {
2714                 updatePrintAttributesFromCapabilities(newCapab);
2715             }
2716 
2717             if (updateNeeded) {
2718                 updatePrintPreviewController(false);
2719             }
2720 
2721             if ((isActive && gotCapab) || (becameActive && hasCapab)) {
2722                 onPrinterAvailable(newPrinterState);
2723             } else if ((becameInactive && hadCabab) || (isActive && lostCapab)) {
2724                 onPrinterUnavailable(newPrinterState);
2725             }
2726 
2727             if (updateNeeded && canUpdateDocument()) {
2728                 updateDocument(false);
2729             }
2730 
2731             // Force a reload of the enabled print services to update mAdvancedPrintOptionsActivity
2732             // in onLoadFinished();
2733             getLoaderManager().getLoader(LOADER_ID_ENABLED_PRINT_SERVICES).forceLoad();
2734 
2735             updateOptionsUi();
2736             updateSummary();
2737         }
2738 
capabilitiesChanged(PrinterCapabilitiesInfo oldCapabilities, PrinterCapabilitiesInfo newCapabilities)2739         private boolean capabilitiesChanged(PrinterCapabilitiesInfo oldCapabilities,
2740                 PrinterCapabilitiesInfo newCapabilities) {
2741             if (oldCapabilities == null) {
2742                 if (newCapabilities != null) {
2743                     return true;
2744                 }
2745             } else if (!oldCapabilities.equals(newCapabilities)) {
2746                 return true;
2747             }
2748             return false;
2749         }
2750     }
2751 
2752     private final class MyOnItemSelectedListener implements AdapterView.OnItemSelectedListener {
2753         @Override
onItemSelected(AdapterView<?> spinner, View view, int position, long id)2754         public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) {
2755             boolean clearRanges = false;
2756 
2757             if (spinner == mDestinationSpinner) {
2758                 if (position == AdapterView.INVALID_POSITION) {
2759                     return;
2760                 }
2761 
2762                 if (id == DEST_ADAPTER_ITEM_ID_MORE) {
2763                     startSelectPrinterActivity();
2764                     return;
2765                 }
2766 
2767                 PrinterHolder currentItem = (PrinterHolder) mDestinationSpinner.getSelectedItem();
2768                 PrinterInfo currentPrinter = (currentItem != null) ? currentItem.printer : null;
2769 
2770                 // Why on earth item selected is called if no selection changed.
2771                 if (mCurrentPrinter == currentPrinter) {
2772                     return;
2773                 }
2774 
2775                 if (mDefaultPrinter == null) {
2776                     mDefaultPrinter = currentPrinter.getId();
2777                 }
2778 
2779                 PrinterId oldId = null;
2780                 if (mCurrentPrinter != null) {
2781                     oldId = mCurrentPrinter.getId();
2782                 }
2783                 mCurrentPrinter = currentPrinter;
2784 
2785                 if (oldId != null) {
2786                     boolean printerRemoved = mDestinationSpinnerAdapter.pruneRemovedPrinter(oldId);
2787 
2788                     if (printerRemoved) {
2789                         // Trigger PrinterObserver.onChanged to adjust selection. This will call
2790                         // this function again.
2791                         mDestinationSpinnerAdapter.notifyDataSetChanged();
2792                         return;
2793                     }
2794 
2795                     if (mState != STATE_INITIALIZING) {
2796                         if (currentPrinter != null) {
2797                             MetricsLogger.action(PrintActivity.this,
2798                                     MetricsEvent.ACTION_PRINTER_SELECT_DROPDOWN,
2799                                     currentPrinter.getId().getServiceName().getPackageName());
2800                         } else {
2801                             MetricsLogger.action(PrintActivity.this,
2802                                     MetricsEvent.ACTION_PRINTER_SELECT_DROPDOWN, "");
2803                         }
2804                     }
2805                 }
2806 
2807                 PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
2808                         currentPrinter.getId());
2809                 if (!printerHolder.removed) {
2810                     setState(STATE_CONFIGURING);
2811                     ensurePreviewUiShown();
2812                 }
2813 
2814                 mPrintJob.setPrinterId(currentPrinter.getId());
2815                 mPrintJob.setPrinterName(currentPrinter.getName());
2816 
2817                 mPrinterRegistry.setTrackedPrinter(currentPrinter.getId());
2818 
2819                 PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities();
2820                 if (capabilities != null) {
2821                     updatePrintAttributesFromCapabilities(capabilities);
2822                 }
2823 
2824                 mPrinterAvailabilityDetector.updatePrinter(currentPrinter);
2825 
2826                 // Force a reload of the enabled print services to update
2827                 // mAdvancedPrintOptionsActivity in onLoadFinished();
2828                 getLoaderManager().getLoader(LOADER_ID_ENABLED_PRINT_SERVICES).forceLoad();
2829             } else if (spinner == mMediaSizeSpinner) {
2830                 SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position);
2831                 PrintAttributes attributes = mPrintJob.getAttributes();
2832 
2833                 MediaSize newMediaSize;
2834                 if (mOrientationSpinner.getSelectedItemPosition() == 0) {
2835                     newMediaSize = mediaItem.value.asPortrait();
2836                 } else {
2837                     newMediaSize = mediaItem.value.asLandscape();
2838                 }
2839 
2840                 if (newMediaSize != attributes.getMediaSize()) {
2841                     if (!newMediaSize.equals(attributes.getMediaSize())
2842                             && !attributes.getMediaSize().equals(MediaSize.UNKNOWN_LANDSCAPE)
2843                             && !attributes.getMediaSize().equals(MediaSize.UNKNOWN_PORTRAIT)
2844                             && mState != STATE_INITIALIZING) {
2845                         MetricsLogger.action(PrintActivity.this,
2846                                 MetricsEvent.ACTION_PRINT_JOB_OPTIONS,
2847                                 PRINT_JOB_OPTIONS_SUBTYPE_MEDIA_SIZE);
2848                     }
2849 
2850                     clearRanges = true;
2851                     attributes.setMediaSize(newMediaSize);
2852                 }
2853             } else if (spinner == mColorModeSpinner) {
2854                 SpinnerItem<Integer> colorModeItem = mColorModeSpinnerAdapter.getItem(position);
2855                 int newMode = colorModeItem.value;
2856 
2857                 if (mPrintJob.getAttributes().getColorMode() != newMode
2858                         && mState != STATE_INITIALIZING) {
2859                     MetricsLogger.action(PrintActivity.this, MetricsEvent.ACTION_PRINT_JOB_OPTIONS,
2860                             PRINT_JOB_OPTIONS_SUBTYPE_COLOR_MODE);
2861                 }
2862 
2863                 mPrintJob.getAttributes().setColorMode(newMode);
2864             } else if (spinner == mDuplexModeSpinner) {
2865                 SpinnerItem<Integer> duplexModeItem = mDuplexModeSpinnerAdapter.getItem(position);
2866                 int newMode = duplexModeItem.value;
2867 
2868                 if (mPrintJob.getAttributes().getDuplexMode() != newMode
2869                         && mState != STATE_INITIALIZING) {
2870                     MetricsLogger.action(PrintActivity.this, MetricsEvent.ACTION_PRINT_JOB_OPTIONS,
2871                             PRINT_JOB_OPTIONS_SUBTYPE_DUPLEX_MODE);
2872                 }
2873 
2874                 mPrintJob.getAttributes().setDuplexMode(newMode);
2875             } else if (spinner == mOrientationSpinner) {
2876                 SpinnerItem<Integer> orientationItem = mOrientationSpinnerAdapter.getItem(position);
2877                 PrintAttributes attributes = mPrintJob.getAttributes();
2878 
2879                 if (mMediaSizeSpinner.getSelectedItem() != null) {
2880                     boolean isPortrait = attributes.isPortrait();
2881                     boolean newIsPortrait = orientationItem.value == ORIENTATION_PORTRAIT;
2882 
2883                     if (isPortrait != newIsPortrait) {
2884                         if (mState != STATE_INITIALIZING) {
2885                             MetricsLogger.action(PrintActivity.this,
2886                                     MetricsEvent.ACTION_PRINT_JOB_OPTIONS,
2887                                     PRINT_JOB_OPTIONS_SUBTYPE_ORIENTATION);
2888                         }
2889 
2890                         clearRanges = true;
2891                         if (newIsPortrait) {
2892                             attributes.copyFrom(attributes.asPortrait());
2893                         } else {
2894                             attributes.copyFrom(attributes.asLandscape());
2895                         }
2896                     }
2897                 }
2898             } else if (spinner == mRangeOptionsSpinner) {
2899                 if (mRangeOptionsSpinner.getSelectedItemPosition() == 0) {
2900                     clearRanges = true;
2901                     mPageRangeEditText.setText("");
2902 
2903                     if (mPageRangeEditText.getVisibility() == View.VISIBLE &&
2904                             mState != STATE_INITIALIZING) {
2905                         MetricsLogger.action(PrintActivity.this,
2906                                 MetricsEvent.ACTION_PRINT_JOB_OPTIONS,
2907                                 PRINT_JOB_OPTIONS_SUBTYPE_PAGE_RANGE);
2908                     }
2909                 } else if (TextUtils.isEmpty(mPageRangeEditText.getText())) {
2910                     mPageRangeEditText.setError("");
2911 
2912                     if (mPageRangeEditText.getVisibility() != View.VISIBLE &&
2913                             mState != STATE_INITIALIZING) {
2914                         MetricsLogger.action(PrintActivity.this,
2915                                 MetricsEvent.ACTION_PRINT_JOB_OPTIONS,
2916                                 PRINT_JOB_OPTIONS_SUBTYPE_PAGE_RANGE);
2917                     }
2918                 }
2919             }
2920 
2921             if (clearRanges) {
2922                 clearPageRanges();
2923             }
2924 
2925             updateOptionsUi();
2926 
2927             if (canUpdateDocument()) {
2928                 updateDocument(false);
2929             }
2930         }
2931 
2932         @Override
onNothingSelected(AdapterView<?> parent)2933         public void onNothingSelected(AdapterView<?> parent) {
2934             /* do nothing*/
2935         }
2936     }
2937 
2938     private final class SelectAllOnFocusListener implements OnFocusChangeListener {
2939         @Override
onFocusChange(View view, boolean hasFocus)2940         public void onFocusChange(View view, boolean hasFocus) {
2941             EditText editText = (EditText) view;
2942             if (!TextUtils.isEmpty(editText.getText())) {
2943                 editText.setSelection(editText.getText().length());
2944             }
2945 
2946             if (view == mPageRangeEditText && !hasFocus && mPageRangeEditText.getError() == null) {
2947                 updateSelectedPagesFromTextField();
2948             }
2949         }
2950     }
2951 
2952     private final class RangeTextWatcher implements TextWatcher {
2953         @Override
onTextChanged(CharSequence s, int start, int before, int count)2954         public void onTextChanged(CharSequence s, int start, int before, int count) {
2955             /* do nothing */
2956         }
2957 
2958         @Override
beforeTextChanged(CharSequence s, int start, int count, int after)2959         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2960             /* do nothing */
2961         }
2962 
2963         @Override
afterTextChanged(Editable editable)2964         public void afterTextChanged(Editable editable) {
2965             final boolean hadErrors = hasErrors();
2966 
2967             PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
2968             final int pageCount = (info != null) ? getAdjustedPageCount(info) : 0;
2969             PageRange[] ranges = PageRangeUtils.parsePageRanges(editable, pageCount);
2970 
2971             if (ranges.length == 0) {
2972                 if (mPageRangeEditText.getError() == null) {
2973                     mPageRangeEditText.setError("");
2974                     updateOptionsUi();
2975                 }
2976                 return;
2977             }
2978 
2979             if (mPageRangeEditText.getError() != null) {
2980                 mPageRangeEditText.setError(null);
2981                 updateOptionsUi();
2982             }
2983 
2984             if (hadErrors && canUpdateDocument()) {
2985                 updateDocument(false);
2986             }
2987         }
2988     }
2989 
2990     private final class EditTextWatcher implements TextWatcher {
2991         @Override
onTextChanged(CharSequence s, int start, int before, int count)2992         public void onTextChanged(CharSequence s, int start, int before, int count) {
2993             /* do nothing */
2994         }
2995 
2996         @Override
beforeTextChanged(CharSequence s, int start, int count, int after)2997         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2998             /* do nothing */
2999         }
3000 
3001         @Override
afterTextChanged(Editable editable)3002         public void afterTextChanged(Editable editable) {
3003             final boolean hadErrors = hasErrors();
3004 
3005             if (editable.length() == 0) {
3006                 if (mCopiesEditText.getError() == null) {
3007                     mCopiesEditText.setError("");
3008                     updateOptionsUi();
3009                 }
3010                 return;
3011             }
3012 
3013             int copies = 0;
3014             try {
3015                 copies = Integer.parseInt(editable.toString());
3016             } catch (NumberFormatException nfe) {
3017                 /* ignore */
3018             }
3019 
3020             if (mState != STATE_INITIALIZING) {
3021                 MetricsLogger.action(PrintActivity.this, MetricsEvent.ACTION_PRINT_JOB_OPTIONS,
3022                         PRINT_JOB_OPTIONS_SUBTYPE_COPIES);
3023             }
3024 
3025             if (copies < MIN_COPIES) {
3026                 if (mCopiesEditText.getError() == null) {
3027                     mCopiesEditText.setError("");
3028                     updateOptionsUi();
3029                 }
3030                 return;
3031             }
3032 
3033             mPrintJob.setCopies(copies);
3034 
3035             if (mCopiesEditText.getError() != null) {
3036                 mCopiesEditText.setError(null);
3037                 updateOptionsUi();
3038             }
3039 
3040             if (hadErrors && canUpdateDocument()) {
3041                 updateDocument(false);
3042             }
3043         }
3044     }
3045 
3046     private final class ProgressMessageController implements Runnable {
3047         private static final long PROGRESS_TIMEOUT_MILLIS = 1000;
3048 
3049         private final Handler mHandler;
3050 
3051         private boolean mPosted;
3052 
3053         /** State before run was executed */
3054         private int mPreviousState = -1;
3055 
ProgressMessageController(Context context)3056         public ProgressMessageController(Context context) {
3057             mHandler = new Handler(context.getMainLooper(), null, false);
3058         }
3059 
post()3060         public void post() {
3061             if (mState == STATE_UPDATE_SLOW) {
3062                 setState(STATE_UPDATE_SLOW);
3063                 ensureProgressUiShown();
3064 
3065                 return;
3066             } else if (mPosted) {
3067                 return;
3068             }
3069             mPreviousState = -1;
3070             mPosted = true;
3071             mHandler.postDelayed(this, PROGRESS_TIMEOUT_MILLIS);
3072         }
3073 
getStateAfterCancel()3074         private int getStateAfterCancel() {
3075             if (mPreviousState == -1) {
3076                 return mState;
3077             } else {
3078                 return mPreviousState;
3079             }
3080         }
3081 
cancel()3082         public int cancel() {
3083             int state;
3084 
3085             if (!mPosted) {
3086                 state = getStateAfterCancel();
3087             } else {
3088                 mPosted = false;
3089                 mHandler.removeCallbacks(this);
3090 
3091                 state = getStateAfterCancel();
3092             }
3093 
3094             mPreviousState = -1;
3095 
3096             return state;
3097         }
3098 
3099         @Override
run()3100         public void run() {
3101             mPosted = false;
3102             mPreviousState = mState;
3103             setState(STATE_UPDATE_SLOW);
3104             ensureProgressUiShown();
3105         }
3106     }
3107 
3108     private static final class DocumentTransformer implements ServiceConnection {
3109         private static final String TEMP_FILE_PREFIX = "print_job";
3110         private static final String TEMP_FILE_EXTENSION = ".pdf";
3111 
3112         private final Context mContext;
3113 
3114         private final MutexFileProvider mFileProvider;
3115 
3116         private final PrintJobInfo mPrintJob;
3117 
3118         private final PageRange[] mPagesToShred;
3119 
3120         private final PrintAttributes mAttributesToApply;
3121 
3122         private final Consumer<String> mCallback;
3123 
3124         private boolean mIsTransformationStarted;
3125 
DocumentTransformer(Context context, PrintJobInfo printJob, MutexFileProvider fileProvider, PrintAttributes attributes, Consumer<String> callback)3126         public DocumentTransformer(Context context, PrintJobInfo printJob,
3127                 MutexFileProvider fileProvider, PrintAttributes attributes,
3128                 Consumer<String> callback) {
3129             mContext = context;
3130             mPrintJob = printJob;
3131             mFileProvider = fileProvider;
3132             mCallback = callback;
3133             mPagesToShred = computePagesToShred(mPrintJob);
3134             mAttributesToApply = attributes;
3135         }
3136 
transform()3137         public void transform() {
3138             // If we have only the pages we want, done.
3139             if (mPagesToShred.length <= 0 && mAttributesToApply == null) {
3140                 mCallback.accept(null);
3141                 return;
3142             }
3143 
3144             // Bind to the manipulation service and the work
3145             // will be performed upon connection to the service.
3146             Intent intent = new Intent(PdfManipulationService.ACTION_GET_EDITOR);
3147             intent.setClass(mContext, PdfManipulationService.class);
3148             mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
3149         }
3150 
3151         @Override
onServiceConnected(ComponentName name, IBinder service)3152         public void onServiceConnected(ComponentName name, IBinder service) {
3153             // We might get several onServiceConnected if the service crashes and restarts.
3154             // mIsTransformationStarted makes sure that we only try once.
3155             if (!mIsTransformationStarted) {
3156                 final IPdfEditor editor = IPdfEditor.Stub.asInterface(service);
3157                 new AsyncTask<Void, Void, String>() {
3158                     @Override
3159                     protected String doInBackground(Void... params) {
3160                         // It's OK to access the data members as they are
3161                         // final and this code is the last one to touch
3162                         // them as shredding is the very last step, so the
3163                         // UI is not interactive at this point.
3164                         try {
3165                             doTransform(editor);
3166                             updatePrintJob();
3167                             return null;
3168                         } catch (IOException | RemoteException | IllegalStateException e) {
3169                             return e.toString();
3170                         }
3171                     }
3172 
3173                     @Override
3174                     protected void onPostExecute(String error) {
3175                         mContext.unbindService(DocumentTransformer.this);
3176                         mCallback.accept(error);
3177                     }
3178                 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
3179 
3180                 mIsTransformationStarted = true;
3181             }
3182         }
3183 
3184         @Override
onServiceDisconnected(ComponentName name)3185         public void onServiceDisconnected(ComponentName name) {
3186             /* do nothing */
3187         }
3188 
doTransform(IPdfEditor editor)3189         private void doTransform(IPdfEditor editor) throws IOException, RemoteException {
3190             File tempFile = null;
3191             ParcelFileDescriptor src = null;
3192             ParcelFileDescriptor dst = null;
3193             InputStream in = null;
3194             OutputStream out = null;
3195             try {
3196                 File jobFile = mFileProvider.acquireFile(null);
3197                 src = ParcelFileDescriptor.open(jobFile, ParcelFileDescriptor.MODE_READ_WRITE);
3198 
3199                 // Open the document.
3200                 editor.openDocument(src);
3201 
3202                 // We passed the fd over IPC, close this one.
3203                 src.close();
3204 
3205                 // Drop the pages.
3206                 editor.removePages(mPagesToShred);
3207 
3208                 // Apply print attributes if needed.
3209                 if (mAttributesToApply != null) {
3210                     editor.applyPrintAttributes(mAttributesToApply);
3211                 }
3212 
3213                 // Write the modified PDF to a temp file.
3214                 tempFile = File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_EXTENSION,
3215                         mContext.getCacheDir());
3216                 dst = ParcelFileDescriptor.open(tempFile, ParcelFileDescriptor.MODE_READ_WRITE);
3217                 editor.write(dst);
3218                 dst.close();
3219 
3220                 // Close the document.
3221                 editor.closeDocument();
3222 
3223                 // Copy the temp file over the print job file.
3224                 jobFile.delete();
3225                 in = new FileInputStream(tempFile);
3226                 out = new FileOutputStream(jobFile);
3227                 Streams.copy(in, out);
3228             } finally {
3229                 IoUtils.closeQuietly(src);
3230                 IoUtils.closeQuietly(dst);
3231                 IoUtils.closeQuietly(in);
3232                 IoUtils.closeQuietly(out);
3233                 if (tempFile != null) {
3234                     tempFile.delete();
3235                 }
3236                 mFileProvider.releaseFile();
3237             }
3238         }
3239 
updatePrintJob()3240         private void updatePrintJob() {
3241             // Update the print job pages.
3242             final int newPageCount = PageRangeUtils.getNormalizedPageCount(
3243                     mPrintJob.getPages(), 0);
3244             mPrintJob.setPages(new PageRange[]{PageRange.ALL_PAGES});
3245 
3246             // Update the print job document info.
3247             PrintDocumentInfo oldDocInfo = mPrintJob.getDocumentInfo();
3248             PrintDocumentInfo newDocInfo = new PrintDocumentInfo
3249                     .Builder(oldDocInfo.getName())
3250                     .setContentType(oldDocInfo.getContentType())
3251                     .setPageCount(newPageCount)
3252                     .build();
3253 
3254             File file = mFileProvider.acquireFile(null);
3255             try {
3256                 newDocInfo.setDataSize(file.length());
3257             } finally {
3258                 mFileProvider.releaseFile();
3259             }
3260 
3261             mPrintJob.setDocumentInfo(newDocInfo);
3262         }
3263 
computePagesToShred(PrintJobInfo printJob)3264         private static PageRange[] computePagesToShred(PrintJobInfo printJob) {
3265             List<PageRange> rangesToShred = new ArrayList<>();
3266             PageRange previousRange = null;
3267 
3268             PageRange[] printedPages = printJob.getPages();
3269             final int rangeCount = printedPages.length;
3270             for (int i = 0; i < rangeCount; i++) {
3271                 PageRange range = printedPages[i];
3272 
3273                 if (previousRange == null) {
3274                     final int startPageIdx = 0;
3275                     final int endPageIdx = range.getStart() - 1;
3276                     if (startPageIdx <= endPageIdx) {
3277                         PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
3278                         rangesToShred.add(removedRange);
3279                     }
3280                 } else {
3281                     final int startPageIdx = previousRange.getEnd() + 1;
3282                     final int endPageIdx = range.getStart() - 1;
3283                     if (startPageIdx <= endPageIdx) {
3284                         PageRange removedRange = new PageRange(startPageIdx, endPageIdx);
3285                         rangesToShred.add(removedRange);
3286                     }
3287                 }
3288 
3289                 if (i == rangeCount - 1) {
3290                     if (range.getEnd() != Integer.MAX_VALUE) {
3291                         rangesToShred.add(new PageRange(range.getEnd() + 1, Integer.MAX_VALUE));
3292                     }
3293                 }
3294 
3295                 previousRange = range;
3296             }
3297 
3298             PageRange[] result = new PageRange[rangesToShred.size()];
3299             rangesToShred.toArray(result);
3300             return result;
3301         }
3302     }
3303 }
3304