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