blob: f71cafe584cccea278ca402a52c1aef1a97faf5f [file] [log] [blame]
Svetoslava798c0a2014-05-15 10:47:19 -07001/*
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
17package com.android.printspooler.ui;
18
19import android.app.Activity;
20import android.app.Fragment;
21import android.app.FragmentTransaction;
22import android.content.ActivityNotFoundException;
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.pm.PackageInfo;
27import android.content.pm.PackageManager.NameNotFoundException;
28import android.content.pm.ResolveInfo;
29import android.database.DataSetObserver;
30import android.graphics.drawable.Drawable;
31import android.net.Uri;
32import android.os.Bundle;
33import android.os.Handler;
34import android.os.IBinder;
35import android.print.IPrintDocumentAdapter;
36import android.print.PageRange;
37import android.print.PrintAttributes;
38import android.print.PrintAttributes.MediaSize;
39import android.print.PrintAttributes.Resolution;
40import android.print.PrintDocumentInfo;
41import android.print.PrintJobInfo;
42import android.print.PrintManager;
43import android.print.PrinterCapabilitiesInfo;
44import android.print.PrinterId;
45import android.print.PrinterInfo;
46import android.printservice.PrintService;
47import android.provider.DocumentsContract;
48import android.text.Editable;
49import android.text.TextUtils;
50import android.text.TextUtils.SimpleStringSplitter;
51import android.text.TextWatcher;
52import android.util.ArrayMap;
53import android.util.Log;
54import android.view.KeyEvent;
55import android.view.View;
56import android.view.View.OnClickListener;
57import android.view.View.OnFocusChangeListener;
58import android.view.ViewGroup;
59import android.view.inputmethod.InputMethodManager;
60import android.widget.AdapterView;
61import android.widget.AdapterView.OnItemSelectedListener;
62import android.widget.ArrayAdapter;
63import android.widget.BaseAdapter;
64import android.widget.Button;
65import android.widget.EditText;
66import android.widget.ImageView;
67import android.widget.Spinner;
68import android.widget.TextView;
69
70import com.android.printspooler.R;
71import com.android.printspooler.model.PrintSpoolerProvider;
72import com.android.printspooler.model.PrintSpoolerService;
73import com.android.printspooler.model.RemotePrintDocument;
74import com.android.printspooler.util.MediaSizeUtils;
75import com.android.printspooler.util.MediaSizeUtils.MediaSizeComparator;
76import com.android.printspooler.util.PageRangeUtils;
77import com.android.printspooler.util.PrintOptionUtils;
78import com.android.printspooler.widget.ContentView;
79import com.android.printspooler.widget.ContentView.OptionsStateChangeListener;
80
81import java.util.*;
82import java.util.regex.Matcher;
83import java.util.regex.Pattern;
84
85public class PrintActivity extends Activity implements RemotePrintDocument.UpdateResultCallbacks,
86 PrintErrorFragment.OnActionListener, PrintProgressFragment.OnCancelRequestListener {
87 private static final String LOG_TAG = "PrintActivity";
88
89 public static final String INTENT_EXTRA_PRINTER_ID = "INTENT_EXTRA_PRINTER_ID";
90
91 private static final int ORIENTATION_PORTRAIT = 0;
92 private static final int ORIENTATION_LANDSCAPE = 1;
93
94 private static final int ACTIVITY_REQUEST_CREATE_FILE = 1;
95 private static final int ACTIVITY_REQUEST_SELECT_PRINTER = 2;
96 private static final int ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS = 3;
97
98 private static final int DEST_ADAPTER_MAX_ITEM_COUNT = 9;
99
100 private static final int DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF = Integer.MAX_VALUE;
101 private static final int DEST_ADAPTER_ITEM_ID_ALL_PRINTERS = Integer.MAX_VALUE - 1;
102
103 private static final int STATE_CONFIGURING = 0;
104 private static final int STATE_PRINT_CONFIRMED = 1;
105 private static final int STATE_PRINT_CANCELED = 2;
106 private static final int STATE_UPDATE_FAILED = 3;
107 private static final int STATE_CREATE_FILE_FAILED = 4;
108 private static final int STATE_PRINTER_UNAVAILABLE = 5;
109
110 private static final int UI_STATE_PREVIEW = 0;
111 private static final int UI_STATE_ERROR = 1;
112 private static final int UI_STATE_PROGRESS = 2;
113
114 private static final int MIN_COPIES = 1;
115 private static final String MIN_COPIES_STRING = String.valueOf(MIN_COPIES);
116
117 private static final Pattern PATTERN_DIGITS = Pattern.compile("[\\d]+");
118
119 private static final Pattern PATTERN_ESCAPE_SPECIAL_CHARS = Pattern.compile(
120 "(?=[]\\[+&|!(){}^\"~*?:\\\\])");
121
122 private static final Pattern PATTERN_PAGE_RANGE = Pattern.compile(
123 "[\\s]*[0-9]*[\\s]*[\\-]?[\\s]*[0-9]*[\\s]*?(([,])"
124 + "[\\s]*[0-9]*[\\s]*[\\-]?[\\s]*[0-9]*[\\s]*|[\\s]*)+");
125
126 public static final PageRange[] ALL_PAGES_ARRAY = new PageRange[] {PageRange.ALL_PAGES};
127
128 private final PrinterAvailabilityDetector mPrinterAvailabilityDetector =
129 new PrinterAvailabilityDetector();
130
131 private final SimpleStringSplitter mStringCommaSplitter = new SimpleStringSplitter(',');
132
133 private final OnFocusChangeListener mSelectAllOnFocusListener = new SelectAllOnFocusListener();
134
135 private PrintSpoolerProvider mSpoolerProvider;
136
137 private PrintJobInfo mPrintJob;
138 private RemotePrintDocument mPrintedDocument;
139 private PrinterRegistry mPrinterRegistry;
140
141 private EditText mCopiesEditText;
142
143 private TextView mPageRangeOptionsTitle;
144 private TextView mPageRangeTitle;
145 private EditText mPageRangeEditText;
146
147 private Spinner mDestinationSpinner;
148 private DestinationAdapter mDestinationSpinnerAdapter;
149
150 private Spinner mMediaSizeSpinner;
151 private ArrayAdapter<SpinnerItem<MediaSize>> mMediaSizeSpinnerAdapter;
152
153 private Spinner mColorModeSpinner;
154 private ArrayAdapter<SpinnerItem<Integer>> mColorModeSpinnerAdapter;
155
156 private Spinner mOrientationSpinner;
157 private ArrayAdapter<SpinnerItem<Integer>> mOrientationSpinnerAdapter;
158
159 private Spinner mRangeOptionsSpinner;
160
161 private ContentView mOptionsContent;
162
163 private TextView mSummaryCopies;
164 private TextView mSummaryPaperSize;
165
166 private View mAdvancedPrintOptionsContainer;
167
168 private Button mMoreOptionsButton;
169
170 private ImageView mPrintButton;
171
172 private ProgressMessageController mProgressMessageController;
173
174 private MediaSizeComparator mMediaSizeComparator;
175
176 private PageRange[] mRequestedPages;
177
178 private PrinterInfo mOldCurrentPrinter;
179
180 private String mCallingPackageName;
181
182 private int mState = STATE_CONFIGURING;
183
184 private int mUiState = UI_STATE_PREVIEW;
185
186 @Override
187 public void onCreate(Bundle savedInstanceState) {
188 super.onCreate(savedInstanceState);
189
190 Bundle extras = getIntent().getExtras();
191
192 mPrintJob = extras.getParcelable(PrintManager.EXTRA_PRINT_JOB);
193 if (mPrintJob == null) {
194 throw new IllegalArgumentException(PrintManager.EXTRA_PRINT_JOB
195 + " cannot be null");
196 }
197 mPrintJob.setAttributes(new PrintAttributes.Builder().build());
198
199 final IBinder adapter = extras.getBinder(PrintManager.EXTRA_PRINT_DOCUMENT_ADAPTER);
200 if (adapter == null) {
201 throw new IllegalArgumentException(PrintManager.EXTRA_PRINT_DOCUMENT_ADAPTER
202 + " cannot be null");
203 }
204
205 mCallingPackageName = extras.getString(DocumentsContract.EXTRA_PACKAGE_NAME);
206
207 // This will take just a few milliseconds, so just wait to
208 // bind to the local service before showing the UI.
209 mSpoolerProvider = new PrintSpoolerProvider(this,
210 new Runnable() {
211 @Override
212 public void run() {
213 // Now that we are bound to the print spooler service,
214 // create the printer registry and wait for it to get
215 // the first batch of results which will be delivered
216 // after reading historical data. This should be pretty
217 // fast, so just wait before showing the UI.
218 mPrinterRegistry = new PrinterRegistry(PrintActivity.this,
219 new Runnable() {
220 @Override
221 public void run() {
222 setTitle(R.string.print_dialog);
223 setContentView(R.layout.print_activity);
224
225 mPrintedDocument = new RemotePrintDocument(PrintActivity.this,
226 IPrintDocumentAdapter.Stub.asInterface(adapter),
227 PrintSpoolerService.generateFileForPrintJob(PrintActivity.this,
228 mPrintJob.getId()),
229 new RemotePrintDocument.DocumentObserver() {
230 @Override
231 public void onDestroy() {
232 finish();
233 }
234 }, PrintActivity.this);
235
236 mProgressMessageController = new ProgressMessageController(PrintActivity.this);
237
238 mMediaSizeComparator = new MediaSizeComparator(PrintActivity.this);
239
240 mDestinationSpinnerAdapter = new DestinationAdapter();
241
242 bindUi();
243
244 updateOptionsUi();
245
246 // Now show the updated UI to avoid flicker.
247 mOptionsContent.setVisibility(View.VISIBLE);
248
249 mRequestedPages = computeRequestedPages();
250
251 mPrintedDocument.start();
252
253 ensurePreviewUiShown();
254 }
255 });
256 }
257 });
258 }
259
260 @Override
261 public void onPause() {
262 if (isFinishing()) {
263 PrintSpoolerService spooler = mSpoolerProvider.getSpooler();
264 if (mState == STATE_PRINT_CONFIRMED) {
265 spooler.updatePrintJobUserConfigurableOptionsNoPersistence(mPrintJob);
266 spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_QUEUED, null);
267 } else {
268 spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_CANCELED, null);
269 }
270 mProgressMessageController.cancel();
271 mPrinterRegistry.setTrackedPrinter(null);
272 mSpoolerProvider.destroy();
273 mPrintedDocument.finish();
274 mPrintedDocument.destroy();
275 }
276
277 mPrinterAvailabilityDetector.cancel();
278
279 super.onPause();
280 }
281
282 @Override
283 public boolean onKeyDown(int keyCode, KeyEvent event) {
284 if (keyCode == KeyEvent.KEYCODE_BACK) {
285 event.startTracking();
286 return true;
287 }
288 return super.onKeyDown(keyCode, event);
289 }
290
291 @Override
292 public boolean onKeyUp(int keyCode, KeyEvent event) {
293 if (keyCode == KeyEvent.KEYCODE_BACK
294 && event.isTracking() && !event.isCanceled()) {
295 cancelPrint();
296 return true;
297 }
298 return super.onKeyUp(keyCode, event);
299 }
300
301 @Override
302 public void onActionPerformed() {
303 switch (mState) {
304 case STATE_UPDATE_FAILED: {
305 if (canUpdateDocument()) {
306 updateDocument(true, true);
307 ensurePreviewUiShown();
308 mState = STATE_CONFIGURING;
309 updateOptionsUi();
310 }
311 } break;
312
313 case STATE_CREATE_FILE_FAILED: {
314 mState = STATE_CONFIGURING;
315 ensurePreviewUiShown();
316 updateOptionsUi();
317 } break;
318 }
319 }
320
321 @Override
322 public void onCancelRequest() {
323 if (mPrintedDocument.isUpdating()) {
324 mPrintedDocument.cancel();
325 }
326 }
327
328 public void onUpdateCanceled() {
329 mProgressMessageController.cancel();
330 ensurePreviewUiShown();
331 finishIfConfirmedOrCanceled();
332 updateOptionsUi();
333 }
334
335 @Override
336 public void onUpdateCompleted(RemotePrintDocument.RemotePrintDocumentInfo document) {
337 mProgressMessageController.cancel();
338 ensurePreviewUiShown();
339
340 // Update the print job with the info for the written document. The page
341 // count we get from the remote document is the pages in the document from
342 // the app perspective but the print job should contain the page count from
343 // print service perspective which is the pages in the written PDF not the
344 // pages in the printed document.
345 PrintDocumentInfo info = document.info;
346 if (info == null) {
347 return;
348 }
349 final int pageCount = PageRangeUtils.getNormalizedPageCount(document.writtenPages,
350 info.getPageCount());
351 PrintDocumentInfo adjustedInfo = new PrintDocumentInfo.Builder(info.getName())
352 .setContentType(info.getContentType())
353 .setPageCount(pageCount)
354 .build();
355 mPrintJob.setDocumentInfo(adjustedInfo);
356 mPrintJob.setPages(document.printedPages);
357 finishIfConfirmedOrCanceled();
358 updateOptionsUi();
359 }
360
361 @Override
362 public void onUpdateFailed(CharSequence error) {
363 mState = STATE_UPDATE_FAILED;
364 ensureErrorUiShown(error, PrintErrorFragment.ACTION_RETRY);
365 updateOptionsUi();
366 }
367
368 @Override
369 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
370 switch (requestCode) {
371 case ACTIVITY_REQUEST_CREATE_FILE: {
372 onStartCreateDocumentActivityResult(resultCode, data);
373 } break;
374
375 case ACTIVITY_REQUEST_SELECT_PRINTER: {
376 onSelectPrinterActivityResult(resultCode, data);
377 } break;
378
379 case ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS: {
380 onAdvancedPrintOptionsActivityResult(resultCode, data);
381 } break;
382 }
383 }
384
385 private void startCreateDocumentActivity() {
386 PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
387 if (info == null) {
388 return;
389 }
390 Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
391 intent.setType("application/pdf");
392 intent.putExtra(Intent.EXTRA_TITLE, info.getName());
393 intent.putExtra(DocumentsContract.EXTRA_PACKAGE_NAME, mCallingPackageName);
394 startActivityForResult(intent, ACTIVITY_REQUEST_CREATE_FILE);
395 }
396
397 private void onStartCreateDocumentActivityResult(int resultCode, Intent data) {
398 if (resultCode == RESULT_OK && data != null) {
399 Uri uri = data.getData();
400 mPrintedDocument.writeContent(getContentResolver(), uri);
401 finish();
402 } else if (resultCode == RESULT_CANCELED) {
403 mState = STATE_CONFIGURING;
404 updateOptionsUi();
405 } else {
406 ensureErrorUiShown(getString(R.string.print_write_error_message),
407 PrintErrorFragment.ACTION_CONFIRM);
408 mState = STATE_CREATE_FILE_FAILED;
409 updateOptionsUi();
410 }
411 }
412
413 private void startSelectPrinterActivity() {
414 Intent intent = new Intent(this, SelectPrinterActivity.class);
415 startActivityForResult(intent, ACTIVITY_REQUEST_SELECT_PRINTER);
416 }
417
418 private void onSelectPrinterActivityResult(int resultCode, Intent data) {
419 if (resultCode == RESULT_OK && data != null) {
420 PrinterId printerId = data.getParcelableExtra(INTENT_EXTRA_PRINTER_ID);
421 if (printerId != null) {
422 mDestinationSpinnerAdapter.ensurePrinterInVisibleAdapterPosition(printerId);
423 final int index = mDestinationSpinnerAdapter.getPrinterIndex(printerId);
424 if (index != AdapterView.INVALID_POSITION) {
425 mDestinationSpinner.setSelection(index);
426 return;
427 }
428 }
429 }
430
431 PrinterId printerId = mOldCurrentPrinter.getId();
432 final int index = mDestinationSpinnerAdapter.getPrinterIndex(printerId);
433 mDestinationSpinner.setSelection(index);
434 }
435
436 private void startAdvancedPrintOptionsActivity(PrinterInfo printer) {
437 ComponentName serviceName = printer.getId().getServiceName();
438
439 String activityName = PrintOptionUtils.getAdvancedOptionsActivityName(this, serviceName);
440 if (TextUtils.isEmpty(activityName)) {
441 return;
442 }
443
444 Intent intent = new Intent(Intent.ACTION_MAIN);
445 intent.setComponent(new ComponentName(serviceName.getPackageName(), activityName));
446
447 List<ResolveInfo> resolvedActivities = getPackageManager()
448 .queryIntentActivities(intent, 0);
449 if (resolvedActivities.isEmpty()) {
450 return;
451 }
452
453 // The activity is a component name, therefore it is one or none.
454 if (resolvedActivities.get(0).activityInfo.exported) {
455 intent.putExtra(PrintService.EXTRA_PRINT_JOB_INFO, mPrintJob);
456 intent.putExtra(PrintService.EXTRA_PRINTER_INFO, printer);
457
458 // This is external activity and may not be there.
459 try {
460 startActivityForResult(intent, ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS);
461 } catch (ActivityNotFoundException anfe) {
462 Log.e(LOG_TAG, "Error starting activity for intent: " + intent, anfe);
463 }
464 }
465 }
466
467 private void onAdvancedPrintOptionsActivityResult(int resultCode, Intent data) {
468 if (resultCode != RESULT_OK || data == null) {
469 return;
470 }
471
472 PrintJobInfo printJobInfo = data.getParcelableExtra(PrintService.EXTRA_PRINT_JOB_INFO);
473
474 if (printJobInfo == null) {
475 return;
476 }
477
478 // Take the advanced options without interpretation.
479 mPrintJob.setAdvancedOptions(printJobInfo.getAdvancedOptions());
480
481 // Take copies without interpretation as the advanced print dialog
482 // cannot create a print job info with invalid copies.
483 mCopiesEditText.setText(String.valueOf(printJobInfo.getCopies()));
484 mPrintJob.setCopies(printJobInfo.getCopies());
485
486 PrintAttributes currAttributes = mPrintJob.getAttributes();
487 PrintAttributes newAttributes = printJobInfo.getAttributes();
488
489 // Take the media size only if the current printer supports is.
490 MediaSize oldMediaSize = currAttributes.getMediaSize();
491 MediaSize newMediaSize = newAttributes.getMediaSize();
492 if (!oldMediaSize.equals(newMediaSize)) {
493 final int mediaSizeCount = mMediaSizeSpinnerAdapter.getCount();
494 MediaSize newMediaSizePortrait = newAttributes.getMediaSize().asPortrait();
495 for (int i = 0; i < mediaSizeCount; i++) {
496 MediaSize supportedSizePortrait = mMediaSizeSpinnerAdapter.getItem(i).value.asPortrait();
497 if (supportedSizePortrait.equals(newMediaSizePortrait)) {
498 currAttributes.setMediaSize(newMediaSize);
499 mMediaSizeSpinner.setSelection(i);
500 if (currAttributes.getMediaSize().isPortrait()) {
501 if (mOrientationSpinner.getSelectedItemPosition() != 0) {
502 mOrientationSpinner.setSelection(0);
503 }
504 } else {
505 if (mOrientationSpinner.getSelectedItemPosition() != 1) {
506 mOrientationSpinner.setSelection(1);
507 }
508 }
509 break;
510 }
511 }
512 }
513
514 // Take the color mode only if the current printer supports it.
515 final int currColorMode = currAttributes.getColorMode();
516 final int newColorMode = newAttributes.getColorMode();
517 if (currColorMode != newColorMode) {
518 final int colorModeCount = mColorModeSpinner.getCount();
519 for (int i = 0; i < colorModeCount; i++) {
520 final int supportedColorMode = mColorModeSpinnerAdapter.getItem(i).value;
521 if (supportedColorMode == newColorMode) {
522 currAttributes.setColorMode(newColorMode);
523 mColorModeSpinner.setSelection(i);
524 break;
525 }
526 }
527 }
528
529 // Take the page range only if it is valid.
530 PageRange[] pageRanges = printJobInfo.getPages();
531 if (pageRanges != null && pageRanges.length > 0) {
532 pageRanges = PageRangeUtils.normalize(pageRanges);
533
534 PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
535 final int pageCount = (info != null) ? info.getPageCount() : 0;
536
537 // Handle the case where all pages are specified explicitly
538 // instead of the *all pages* constant.
539 if (pageRanges.length == 1) {
540 if (pageRanges[0].getStart() == 0 && pageRanges[0].getEnd() == pageCount - 1) {
541 pageRanges[0] = PageRange.ALL_PAGES;
542 }
543 }
544
545 if (Arrays.equals(pageRanges, ALL_PAGES_ARRAY)) {
546 mPrintJob.setPages(pageRanges);
547
548 if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
549 mRangeOptionsSpinner.setSelection(0);
550 }
551 } else if (pageRanges[0].getStart() >= 0
552 && pageRanges[pageRanges.length - 1].getEnd() < pageCount) {
553 mPrintJob.setPages(pageRanges);
554
555 if (mRangeOptionsSpinner.getSelectedItemPosition() != 1) {
556 mRangeOptionsSpinner.setSelection(1);
557 }
558
559 StringBuilder builder = new StringBuilder();
560 final int pageRangeCount = pageRanges.length;
561 for (int i = 0; i < pageRangeCount; i++) {
562 if (builder.length() > 0) {
563 builder.append(',');
564 }
565
566 final int shownStartPage;
567 final int shownEndPage;
568 PageRange pageRange = pageRanges[i];
569 if (pageRange.equals(PageRange.ALL_PAGES)) {
570 shownStartPage = 1;
571 shownEndPage = pageCount;
572 } else {
573 shownStartPage = pageRange.getStart() + 1;
574 shownEndPage = pageRange.getEnd() + 1;
575 }
576
577 builder.append(shownStartPage);
578
579 if (shownStartPage != shownEndPage) {
580 builder.append('-');
581 builder.append(shownEndPage);
582 }
583 }
584 mPageRangeEditText.setText(builder.toString());
585 }
586 }
587
588 // Update the content if needed.
589 if (canUpdateDocument()) {
590 updateDocument(true, false);
591 }
592 }
593
594 private void ensureProgressUiShown() {
595 if (mUiState != UI_STATE_PROGRESS) {
596 mUiState = UI_STATE_PROGRESS;
597 Fragment fragment = PrintProgressFragment.newInstance();
598 showFragment(fragment);
599 }
600 }
601
602 private void ensurePreviewUiShown() {
603 if (mUiState != UI_STATE_PREVIEW) {
604 mUiState = UI_STATE_PREVIEW;
605 Fragment fragment = PrintPreviewFragment.newInstance();
606 showFragment(fragment);
607 }
608 }
609
610 private void ensureErrorUiShown(CharSequence message, int action) {
611 if (mUiState != UI_STATE_ERROR) {
612 mUiState = UI_STATE_ERROR;
613 Fragment fragment = PrintErrorFragment.newInstance(message, action);
614 showFragment(fragment);
615 }
616 }
617
618 private void showFragment(Fragment fragment) {
619 FragmentTransaction transaction = getFragmentManager().beginTransaction();
620 Fragment oldFragment = getFragmentManager().findFragmentById(
621 R.id.embedded_content_container);
622 if (oldFragment != null) {
623 transaction.remove(oldFragment);
624 }
625 transaction.add(R.id.embedded_content_container, fragment);
626 transaction.commit();
627 }
628
629 private void requestCreatePdfFileOrFinish() {
630 if (getCurrentPrinter() == mDestinationSpinnerAdapter.getPdfPrinter()) {
631 startCreateDocumentActivity();
632 } else {
633 finish();
634 }
635 }
636
637 private void finishIfConfirmedOrCanceled() {
638 if (mState == STATE_PRINT_CONFIRMED) {
639 requestCreatePdfFileOrFinish();
640 } else if (mState == STATE_PRINT_CANCELED) {
641 finish();
642 }
643 }
644
645 private void updatePrintAttributesFromCapabilities(PrinterCapabilitiesInfo capabilities) {
646 PrintAttributes defaults = capabilities.getDefaults();
647
648 // Sort the media sizes based on the current locale.
649 List<MediaSize> sortedMediaSizes = new ArrayList<>(capabilities.getMediaSizes());
650 Collections.sort(sortedMediaSizes, mMediaSizeComparator);
651
652 PrintAttributes attributes = mPrintJob.getAttributes();
653
654 // Media size.
655 MediaSize currMediaSize = attributes.getMediaSize();
656 if (currMediaSize == null) {
657 attributes.setMediaSize(defaults.getMediaSize());
658 } else {
659 boolean foundCurrentMediaSize = false;
660 // Try to find the current media size in the capabilities as
661 // it may be in a different orientation.
662 MediaSize currMediaSizePortrait = currMediaSize.asPortrait();
663 final int mediaSizeCount = sortedMediaSizes.size();
664 for (int i = 0; i < mediaSizeCount; i++) {
665 MediaSize mediaSize = sortedMediaSizes.get(i);
666 if (currMediaSizePortrait.equals(mediaSize.asPortrait())) {
667 attributes.setMediaSize(currMediaSize);
668 foundCurrentMediaSize = true;
669 break;
670 }
671 }
672 // If we did not find the current media size fall back to default.
673 if (!foundCurrentMediaSize) {
674 attributes.setMediaSize(defaults.getMediaSize());
675 }
676 }
677
678 // Color mode.
679 final int colorMode = attributes.getColorMode();
680 if ((capabilities.getColorModes() & colorMode) == 0) {
681 attributes.setColorMode(defaults.getColorMode());
682 }
683
684 // Resolution
685 Resolution resolution = attributes.getResolution();
686 if (resolution == null || !capabilities.getResolutions().contains(resolution)) {
687 attributes.setResolution(defaults.getResolution());
688 }
689
690 // Margins.
691 attributes.setMinMargins(defaults.getMinMargins());
692 }
693
694 private boolean updateDocument(boolean preview, boolean clearLastError) {
695 if (!clearLastError && mPrintedDocument.hasUpdateError()) {
696 return false;
697 }
698
699 if (clearLastError && mPrintedDocument.hasUpdateError()) {
700 mPrintedDocument.clearUpdateError();
701 }
702
703 if (mRequestedPages != null && mRequestedPages.length > 0) {
704 final PageRange[] pages;
705 if (preview) {
706 final int firstPage = mRequestedPages[0].getStart();
707 pages = new PageRange[]{new PageRange(firstPage, firstPage)};
708 } else {
709 pages = mRequestedPages;
710 }
711 final boolean willUpdate = mPrintedDocument.update(mPrintJob.getAttributes(),
712 pages, preview);
713
714 if (willUpdate) {
715 mProgressMessageController.post();
716 return true;
717 }
718 }
719
720 return false;
721 }
722
723 private void addCurrentPrinterToHistory() {
724 PrinterInfo currentPrinter = getCurrentPrinter();
725 if (currentPrinter != null) {
726 PrinterId fakePdfPrinterId = mDestinationSpinnerAdapter.getPdfPrinter().getId();
727 if (!currentPrinter.getId().equals(fakePdfPrinterId)) {
728 mPrinterRegistry.addHistoricalPrinter(currentPrinter);
729 }
730 }
731 }
732
733 private PrinterInfo getCurrentPrinter() {
734 return ((PrinterHolder) mDestinationSpinner.getSelectedItem()).printer;
735 }
736
737 private void cancelPrint() {
738 mState = STATE_PRINT_CANCELED;
739 updateOptionsUi();
740 if (mPrintedDocument.isUpdating()) {
741 mPrintedDocument.cancel();
742 }
743 finish();
744 }
745
746 private void confirmPrint() {
747 mState = STATE_PRINT_CONFIRMED;
748 updateOptionsUi();
749 if (canUpdateDocument()) {
750 updateDocument(false, false);
751 }
752 addCurrentPrinterToHistory();
753 if (!mPrintedDocument.isUpdating()) {
754 requestCreatePdfFileOrFinish();
755 }
756 }
757
758 private void bindUi() {
759 // Summary
760 mSummaryCopies = (TextView) findViewById(R.id.copies_count_summary);
761 mSummaryPaperSize = (TextView) findViewById(R.id.paper_size_summary);
762
763 // Options container
764 mOptionsContent = (ContentView) findViewById(R.id.options_content);
765 mOptionsContent.setOptionsStateChangeListener(new OptionsStateChangeListener() {
766 @Override
767 public void onOptionsOpened() {
768 // TODO: Update preview.
769 }
770
771 @Override
772 public void onOptionsClosed() {
773 // TODO: Update preview.
774 }
775 });
776
777 OnItemSelectedListener itemSelectedListener = new MyOnItemSelectedListener();
778 OnClickListener clickListener = new MyClickListener();
779
780 // Copies
781 mCopiesEditText = (EditText) findViewById(R.id.copies_edittext);
782 mCopiesEditText.setOnFocusChangeListener(mSelectAllOnFocusListener);
783 mCopiesEditText.setText(MIN_COPIES_STRING);
784 mCopiesEditText.setSelection(mCopiesEditText.getText().length());
785 mCopiesEditText.addTextChangedListener(new EditTextWatcher());
786
787 // Destination.
788 mDestinationSpinnerAdapter.registerDataSetObserver(new PrintersObserver());
789 mDestinationSpinner = (Spinner) findViewById(R.id.destination_spinner);
790 mDestinationSpinner.setAdapter(mDestinationSpinnerAdapter);
791 mDestinationSpinner.setOnItemSelectedListener(itemSelectedListener);
792 mDestinationSpinner.setSelection(0);
793
794 // Media size.
795 mMediaSizeSpinnerAdapter = new ArrayAdapter<>(
796 this, R.layout.spinner_dropdown_item, R.id.title);
797 mMediaSizeSpinner = (Spinner) findViewById(R.id.paper_size_spinner);
798 mMediaSizeSpinner.setAdapter(mMediaSizeSpinnerAdapter);
799 mMediaSizeSpinner.setOnItemSelectedListener(itemSelectedListener);
800
801 // Color mode.
802 mColorModeSpinnerAdapter = new ArrayAdapter<>(
803 this, R.layout.spinner_dropdown_item, R.id.title);
804 mColorModeSpinner = (Spinner) findViewById(R.id.color_spinner);
805 mColorModeSpinner.setAdapter(mColorModeSpinnerAdapter);
806 mColorModeSpinner.setOnItemSelectedListener(itemSelectedListener);
807
808 // Orientation
809 mOrientationSpinnerAdapter = new ArrayAdapter<>(
810 this, R.layout.spinner_dropdown_item, R.id.title);
811 String[] orientationLabels = getResources().getStringArray(
812 R.array.orientation_labels);
813 mOrientationSpinnerAdapter.add(new SpinnerItem<>(
814 ORIENTATION_PORTRAIT, orientationLabels[0]));
815 mOrientationSpinnerAdapter.add(new SpinnerItem<>(
816 ORIENTATION_LANDSCAPE, orientationLabels[1]));
817 mOrientationSpinner = (Spinner) findViewById(R.id.orientation_spinner);
818 mOrientationSpinner.setAdapter(mOrientationSpinnerAdapter);
819 mOrientationSpinner.setOnItemSelectedListener(itemSelectedListener);
820
821 // Range options
822 ArrayAdapter<SpinnerItem<Integer>> rangeOptionsSpinnerAdapter =
823 new ArrayAdapter<>(this, R.layout.spinner_dropdown_item, R.id.title);
824 final int[] rangeOptionsValues = getResources().getIntArray(
825 R.array.page_options_values);
826 String[] rangeOptionsLabels = getResources().getStringArray(
827 R.array.page_options_labels);
828 final int rangeOptionsCount = rangeOptionsLabels.length;
829 for (int i = 0; i < rangeOptionsCount; i++) {
830 rangeOptionsSpinnerAdapter.add(new SpinnerItem<>(
831 rangeOptionsValues[i], rangeOptionsLabels[i]));
832 }
833 mPageRangeOptionsTitle = (TextView) findViewById(R.id.range_options_title);
834 mRangeOptionsSpinner = (Spinner) findViewById(R.id.range_options_spinner);
835 mRangeOptionsSpinner.setAdapter(rangeOptionsSpinnerAdapter);
836 mRangeOptionsSpinner.setOnItemSelectedListener(itemSelectedListener);
837
838 // Page range
839 mPageRangeTitle = (TextView) findViewById(R.id.page_range_title);
840 mPageRangeEditText = (EditText) findViewById(R.id.page_range_edittext);
841 mPageRangeEditText.setOnFocusChangeListener(mSelectAllOnFocusListener);
842 mPageRangeEditText.addTextChangedListener(new RangeTextWatcher());
843
844 // Advanced options button.
845 mAdvancedPrintOptionsContainer = findViewById(R.id.more_options_container);
846 mMoreOptionsButton = (Button) findViewById(R.id.more_options_button);
847 mMoreOptionsButton.setOnClickListener(new OnClickListener() {
848 @Override
849 public void onClick(View v) {
850 PrinterInfo currentPrinter = getCurrentPrinter();
851 if (currentPrinter != null) {
852 startAdvancedPrintOptionsActivity(currentPrinter);
853 }
854 }
855 });
856
857 // Print button
858 mPrintButton = (ImageView) findViewById(R.id.print_button);
859 mPrintButton.setOnClickListener(clickListener);
860 }
861
862 private final class MyClickListener implements OnClickListener {
863 @Override
864 public void onClick(View view) {
865 if (view == mPrintButton) {
866 PrinterInfo currentPrinter = getCurrentPrinter();
867 if (currentPrinter != null) {
868 confirmPrint();
869 } else {
870 cancelPrint();
871 }
872 } else if (view == mMoreOptionsButton) {
873 PrinterInfo currentPrinter = getCurrentPrinter();
874 if (currentPrinter != null) {
875 startAdvancedPrintOptionsActivity(currentPrinter);
876 }
877 }
878 }
879 }
880
881 private static boolean canPrint(PrinterInfo printer) {
882 return printer.getCapabilities() != null
883 && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
884 }
885
886 private void updateOptionsUi() {
887 // Always update the summary.
888 if (!TextUtils.isEmpty(mCopiesEditText.getText())) {
889 mSummaryCopies.setText(mCopiesEditText.getText());
890 }
891
892 final int selectedMediaIndex = mMediaSizeSpinner.getSelectedItemPosition();
893 if (selectedMediaIndex >= 0) {
894 SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(selectedMediaIndex);
895 mSummaryPaperSize.setText(mediaItem.label);
896 }
897
898 if (mState == STATE_PRINT_CONFIRMED
899 || mState == STATE_PRINT_CANCELED
900 || mState == STATE_UPDATE_FAILED
901 || mState == STATE_CREATE_FILE_FAILED
902 || mState == STATE_PRINTER_UNAVAILABLE) {
903 if (mState != STATE_PRINTER_UNAVAILABLE) {
904 mDestinationSpinner.setEnabled(false);
905 }
906 mCopiesEditText.setEnabled(false);
907 mMediaSizeSpinner.setEnabled(false);
908 mColorModeSpinner.setEnabled(false);
909 mOrientationSpinner.setEnabled(false);
910 mRangeOptionsSpinner.setEnabled(false);
911 mPageRangeEditText.setEnabled(false);
912 mPrintButton.setEnabled(false);
913 mMoreOptionsButton.setEnabled(false);
914 return;
915 }
916
917 // If no current printer, or it has no capabilities, or it is not
918 // available, we disable all print options except the destination.
919 PrinterInfo currentPrinter = getCurrentPrinter();
920 if (currentPrinter == null || !canPrint(currentPrinter)) {
921 mCopiesEditText.setEnabled(false);
922 mMediaSizeSpinner.setEnabled(false);
923 mColorModeSpinner.setEnabled(false);
924 mOrientationSpinner.setEnabled(false);
925 mRangeOptionsSpinner.setEnabled(false);
926 mPageRangeEditText.setEnabled(false);
927 mPrintButton.setEnabled(false);
928 mMoreOptionsButton.setEnabled(false);
929 return;
930 }
931
932 PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities();
933 PrintAttributes defaultAttributes = capabilities.getDefaults();
934
935 // Media size.
936 mMediaSizeSpinner.setEnabled(true);
937
938 List<MediaSize> mediaSizes = new ArrayList<>(capabilities.getMediaSizes());
939 // Sort the media sizes based on the current locale.
940 Collections.sort(mediaSizes, mMediaSizeComparator);
941
942 PrintAttributes attributes = mPrintJob.getAttributes();
943
944 // If the media sizes changed, we update the adapter and the spinner.
945 boolean mediaSizesChanged = false;
946 final int mediaSizeCount = mediaSizes.size();
947 if (mediaSizeCount != mMediaSizeSpinnerAdapter.getCount()) {
948 mediaSizesChanged = true;
949 } else {
950 for (int i = 0; i < mediaSizeCount; i++) {
951 if (!mediaSizes.get(i).equals(mMediaSizeSpinnerAdapter.getItem(i).value)) {
952 mediaSizesChanged = true;
953 break;
954 }
955 }
956 }
957 if (mediaSizesChanged) {
958 // Remember the old media size to try selecting it again.
959 int oldMediaSizeNewIndex = AdapterView.INVALID_POSITION;
960 MediaSize oldMediaSize = attributes.getMediaSize();
961
962 // Rebuild the adapter data.
963 mMediaSizeSpinnerAdapter.clear();
964 for (int i = 0; i < mediaSizeCount; i++) {
965 MediaSize mediaSize = mediaSizes.get(i);
966 if (oldMediaSize != null
967 && mediaSize.asPortrait().equals(oldMediaSize.asPortrait())) {
968 // Update the index of the old selection.
969 oldMediaSizeNewIndex = i;
970 }
971 mMediaSizeSpinnerAdapter.add(new SpinnerItem<>(
972 mediaSize, mediaSize.getLabel(getPackageManager())));
973 }
974
975 if (oldMediaSizeNewIndex != AdapterView.INVALID_POSITION) {
976 // Select the old media size - nothing really changed.
977 if (mMediaSizeSpinner.getSelectedItemPosition() != oldMediaSizeNewIndex) {
978 mMediaSizeSpinner.setSelection(oldMediaSizeNewIndex);
979 }
980 } else {
981 // Select the first or the default.
982 final int mediaSizeIndex = Math.max(mediaSizes.indexOf(
983 defaultAttributes.getMediaSize()), 0);
984 if (mMediaSizeSpinner.getSelectedItemPosition() != mediaSizeIndex) {
985 mMediaSizeSpinner.setSelection(mediaSizeIndex);
986 }
987 // Respect the orientation of the old selection.
988 if (oldMediaSize != null) {
989 if (oldMediaSize.isPortrait()) {
990 attributes.setMediaSize(mMediaSizeSpinnerAdapter
991 .getItem(mediaSizeIndex).value.asPortrait());
992 } else {
993 attributes.setMediaSize(mMediaSizeSpinnerAdapter
994 .getItem(mediaSizeIndex).value.asLandscape());
995 }
996 }
997 }
998 }
999
1000 // Color mode.
1001 mColorModeSpinner.setEnabled(true);
1002 final int colorModes = capabilities.getColorModes();
1003
1004 // If the color modes changed, we update the adapter and the spinner.
1005 boolean colorModesChanged = false;
1006 if (Integer.bitCount(colorModes) != mColorModeSpinnerAdapter.getCount()) {
1007 colorModesChanged = true;
1008 } else {
1009 int remainingColorModes = colorModes;
1010 int adapterIndex = 0;
1011 while (remainingColorModes != 0) {
1012 final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
1013 final int colorMode = 1 << colorBitOffset;
1014 remainingColorModes &= ~colorMode;
1015 if (colorMode != mColorModeSpinnerAdapter.getItem(adapterIndex).value) {
1016 colorModesChanged = true;
1017 break;
1018 }
1019 adapterIndex++;
1020 }
1021 }
1022 if (colorModesChanged) {
1023 // Remember the old color mode to try selecting it again.
1024 int oldColorModeNewIndex = AdapterView.INVALID_POSITION;
1025 final int oldColorMode = attributes.getColorMode();
1026
1027 // Rebuild the adapter data.
1028 mColorModeSpinnerAdapter.clear();
1029 String[] colorModeLabels = getResources().getStringArray(R.array.color_mode_labels);
1030 int remainingColorModes = colorModes;
1031 while (remainingColorModes != 0) {
1032 final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
1033 final int colorMode = 1 << colorBitOffset;
1034 if (colorMode == oldColorMode) {
1035 // Update the index of the old selection.
1036 oldColorModeNewIndex = colorBitOffset;
1037 }
1038 remainingColorModes &= ~colorMode;
1039 mColorModeSpinnerAdapter.add(new SpinnerItem<>(colorMode,
1040 colorModeLabels[colorBitOffset]));
1041 }
1042 if (oldColorModeNewIndex != AdapterView.INVALID_POSITION) {
1043 // Select the old color mode - nothing really changed.
1044 if (mColorModeSpinner.getSelectedItemPosition() != oldColorModeNewIndex) {
1045 mColorModeSpinner.setSelection(oldColorModeNewIndex);
1046 }
1047 } else {
1048 // Select the default.
1049 final int selectedColorMode = colorModes & defaultAttributes.getColorMode();
1050 final int itemCount = mColorModeSpinnerAdapter.getCount();
1051 for (int i = 0; i < itemCount; i++) {
1052 SpinnerItem<Integer> item = mColorModeSpinnerAdapter.getItem(i);
1053 if (selectedColorMode == item.value) {
1054 if (mColorModeSpinner.getSelectedItemPosition() != i) {
1055 mColorModeSpinner.setSelection(i);
1056 }
1057 attributes.setColorMode(selectedColorMode);
1058 }
1059 }
1060 }
1061 }
1062
1063 // Orientation
1064 mOrientationSpinner.setEnabled(true);
1065 MediaSize mediaSize = attributes.getMediaSize();
1066 if (mediaSize != null) {
1067 if (mediaSize.isPortrait()
1068 && mOrientationSpinner.getSelectedItemPosition() != 0) {
1069 mOrientationSpinner.setSelection(0);
1070 } else if (!mediaSize.isPortrait()
1071 && mOrientationSpinner.getSelectedItemPosition() != 1) {
1072 mOrientationSpinner.setSelection(1);
1073 }
1074 }
1075
1076 // Range options
1077 PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
1078 if (info != null && info.getPageCount() > 0) {
1079 if (info.getPageCount() == 1) {
1080 mRangeOptionsSpinner.setEnabled(false);
1081 } else {
1082 mRangeOptionsSpinner.setEnabled(true);
1083 if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
1084 if (!mPageRangeEditText.isEnabled()) {
1085 mPageRangeEditText.setEnabled(true);
1086 mPageRangeEditText.setVisibility(View.VISIBLE);
1087 mPageRangeTitle.setVisibility(View.VISIBLE);
1088 mPageRangeEditText.requestFocus();
1089 InputMethodManager imm = (InputMethodManager)
1090 getSystemService(Context.INPUT_METHOD_SERVICE);
1091 imm.showSoftInput(mPageRangeEditText, 0);
1092 }
1093 } else {
1094 mPageRangeEditText.setEnabled(false);
1095 mPageRangeEditText.setVisibility(View.INVISIBLE);
1096 mPageRangeTitle.setVisibility(View.INVISIBLE);
1097 }
1098 }
1099 String title = (info.getPageCount() != PrintDocumentInfo.PAGE_COUNT_UNKNOWN)
1100 ? getString(R.string.label_pages, String.valueOf(info.getPageCount()))
1101 : getString(R.string.page_count_unknown);
1102 mPageRangeOptionsTitle.setText(title);
1103 } else {
1104 if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
1105 mRangeOptionsSpinner.setSelection(0);
1106 }
1107 mRangeOptionsSpinner.setEnabled(false);
1108 mPageRangeOptionsTitle.setText(getString(R.string.page_count_unknown));
1109 mPageRangeEditText.setEnabled(false);
1110 mPageRangeEditText.setVisibility(View.INVISIBLE);
1111 mPageRangeTitle.setVisibility(View.INVISIBLE);
1112 }
1113
1114 // Advanced print options
1115 ComponentName serviceName = currentPrinter.getId().getServiceName();
1116 if (!TextUtils.isEmpty(PrintOptionUtils.getAdvancedOptionsActivityName(
1117 this, serviceName))) {
1118 mAdvancedPrintOptionsContainer.setVisibility(View.VISIBLE);
1119 mMoreOptionsButton.setEnabled(true);
1120 } else {
1121 mAdvancedPrintOptionsContainer.setVisibility(View.GONE);
1122 mMoreOptionsButton.setEnabled(false);
1123 }
1124
1125 // Print
1126 if (mDestinationSpinnerAdapter.getPdfPrinter() != currentPrinter) {
1127 mPrintButton.setImageResource(com.android.internal.R.drawable.ic_print);
1128 } else {
1129 mPrintButton.setImageResource(com.android.internal.R.drawable.ic_menu_save);
1130 }
1131 if ((mRangeOptionsSpinner.getSelectedItemPosition() == 1
1132 && (TextUtils.isEmpty(mPageRangeEditText.getText()) || hasErrors()))
1133 || (mRangeOptionsSpinner.getSelectedItemPosition() == 0
1134 && (mPrintedDocument.getDocumentInfo() == null || hasErrors()))) {
1135 mPrintButton.setEnabled(false);
1136 } else {
1137 mPrintButton.setEnabled(true);
1138 }
1139
1140 // Copies
1141 if (mDestinationSpinnerAdapter.getPdfPrinter() != currentPrinter) {
1142 mCopiesEditText.setEnabled(true);
1143 } else {
1144 mCopiesEditText.setEnabled(false);
1145 }
1146 if (mCopiesEditText.getError() == null
1147 && TextUtils.isEmpty(mCopiesEditText.getText())) {
1148 mCopiesEditText.setText(String.valueOf(MIN_COPIES));
1149 mCopiesEditText.requestFocus();
1150 }
1151 }
1152
1153 private PageRange[] computeRequestedPages() {
1154 if (hasErrors()) {
1155 return null;
1156 }
1157
1158 if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
1159 List<PageRange> pageRanges = new ArrayList<>();
1160 mStringCommaSplitter.setString(mPageRangeEditText.getText().toString());
1161
1162 while (mStringCommaSplitter.hasNext()) {
1163 String range = mStringCommaSplitter.next().trim();
1164 if (TextUtils.isEmpty(range)) {
1165 continue;
1166 }
1167 final int dashIndex = range.indexOf('-');
1168 final int fromIndex;
1169 final int toIndex;
1170
1171 if (dashIndex > 0) {
1172 fromIndex = Integer.parseInt(range.substring(0, dashIndex).trim()) - 1;
1173 // It is possible that the dash is at the end since the input
1174 // verification can has to allow the user to keep entering if
1175 // this would lead to a valid input. So we handle this.
1176 if (dashIndex < range.length() - 1) {
1177 String fromString = range.substring(dashIndex + 1, range.length()).trim();
1178 toIndex = Integer.parseInt(fromString) - 1;
1179 } else {
1180 toIndex = fromIndex;
1181 }
1182 } else {
1183 fromIndex = toIndex = Integer.parseInt(range) - 1;
1184 }
1185
1186 PageRange pageRange = new PageRange(Math.min(fromIndex, toIndex),
1187 Math.max(fromIndex, toIndex));
1188 pageRanges.add(pageRange);
1189 }
1190
1191 PageRange[] pageRangesArray = new PageRange[pageRanges.size()];
1192 pageRanges.toArray(pageRangesArray);
1193
1194 return PageRangeUtils.normalize(pageRangesArray);
1195 }
1196
1197 return ALL_PAGES_ARRAY;
1198 }
1199
1200 private boolean hasErrors() {
1201 return (mCopiesEditText.getError() != null)
1202 || (mPageRangeEditText.getVisibility() == View.VISIBLE
1203 && mPageRangeEditText.getError() != null);
1204 }
1205
1206 public void onPrinterAvailable(PrinterInfo printer) {
1207 PrinterInfo currentPrinter = getCurrentPrinter();
1208 if (currentPrinter.equals(printer)) {
1209 mState = STATE_CONFIGURING;
1210 if (canUpdateDocument()) {
1211 updateDocument(true, false);
1212 }
1213 ensurePreviewUiShown();
1214 updateOptionsUi();
1215 }
1216 }
1217
1218 public void onPrinterUnavailable(PrinterInfo printer) {
1219 if (getCurrentPrinter().getId().equals(printer.getId())) {
1220 mState = STATE_PRINTER_UNAVAILABLE;
1221 if (mPrintedDocument.isUpdating()) {
1222 mPrintedDocument.cancel();
1223 }
1224 ensureErrorUiShown(getString(R.string.print_error_printer_unavailable),
1225 PrintErrorFragment.ACTION_NONE);
1226 updateOptionsUi();
1227 }
1228 }
1229
1230 private final class SpinnerItem<T> {
1231 final T value;
1232 final CharSequence label;
1233
1234 public SpinnerItem(T value, CharSequence label) {
1235 this.value = value;
1236 this.label = label;
1237 }
1238
1239 public String toString() {
1240 return label.toString();
1241 }
1242 }
1243
1244 private final class PrinterAvailabilityDetector implements Runnable {
1245 private static final long UNAVAILABLE_TIMEOUT_MILLIS = 10000; // 10sec
1246
1247 private boolean mPosted;
1248
1249 private boolean mPrinterUnavailable;
1250
1251 private PrinterInfo mPrinter;
1252
1253 public void updatePrinter(PrinterInfo printer) {
1254 if (printer.equals(mDestinationSpinnerAdapter.getPdfPrinter())) {
1255 return;
1256 }
1257
1258 final boolean available = printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE
1259 && printer.getCapabilities() != null;
1260 final boolean notifyIfAvailable;
1261
1262 if (mPrinter == null || !mPrinter.getId().equals(printer.getId())) {
1263 notifyIfAvailable = true;
1264 unpostIfNeeded();
1265 mPrinterUnavailable = false;
1266 mPrinter = new PrinterInfo.Builder(printer).build();
1267 } else {
1268 notifyIfAvailable =
1269 (mPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE
1270 && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE)
1271 || (mPrinter.getCapabilities() == null
1272 && printer.getCapabilities() != null);
1273 mPrinter.copyFrom(printer);
1274 }
1275
1276 if (available) {
1277 unpostIfNeeded();
1278 mPrinterUnavailable = false;
1279 if (notifyIfAvailable) {
1280 onPrinterAvailable(mPrinter);
1281 }
1282 } else {
1283 if (!mPrinterUnavailable) {
1284 postIfNeeded();
1285 }
1286 }
1287 }
1288
1289 public void cancel() {
1290 unpostIfNeeded();
1291 mPrinterUnavailable = false;
1292 }
1293
1294 private void postIfNeeded() {
1295 if (!mPosted) {
1296 mPosted = true;
1297 mDestinationSpinner.postDelayed(this, UNAVAILABLE_TIMEOUT_MILLIS);
1298 }
1299 }
1300
1301 private void unpostIfNeeded() {
1302 if (mPosted) {
1303 mPosted = false;
1304 mDestinationSpinner.removeCallbacks(this);
1305 }
1306 }
1307
1308 @Override
1309 public void run() {
1310 mPosted = false;
1311 mPrinterUnavailable = true;
1312 onPrinterUnavailable(mPrinter);
1313 }
1314 }
1315
1316 private static final class PrinterHolder {
1317 PrinterInfo printer;
1318 boolean removed;
1319
1320 public PrinterHolder(PrinterInfo printer) {
1321 this.printer = printer;
1322 }
1323 }
1324
1325 private final class DestinationAdapter extends BaseAdapter
1326 implements PrinterRegistry.OnPrintersChangeListener {
1327 private final List<PrinterHolder> mPrinterHolders = new ArrayList<>();
1328
1329 private final PrinterHolder mFakePdfPrinterHolder;
1330
1331 public DestinationAdapter() {
1332 addPrinters(mPrinterHolders, mPrinterRegistry.getPrinters());
1333 mPrinterRegistry.setOnPrintersChangeListener(this);
1334 mFakePdfPrinterHolder = new PrinterHolder(createFakePdfPrinter());
1335 }
1336
1337 public PrinterInfo getPdfPrinter() {
1338 return mFakePdfPrinterHolder.printer;
1339 }
1340
1341 public int getPrinterIndex(PrinterId printerId) {
1342 for (int i = 0; i < getCount(); i++) {
1343 PrinterHolder printerHolder = (PrinterHolder) getItem(i);
1344 if (printerHolder != null && !printerHolder.removed
1345 && printerHolder.printer.getId().equals(printerId)) {
1346 return i;
1347 }
1348 }
1349 return AdapterView.INVALID_POSITION;
1350 }
1351
1352 public void ensurePrinterInVisibleAdapterPosition(PrinterId printerId) {
1353 final int printerCount = mPrinterHolders.size();
1354 for (int i = 0; i < printerCount; i++) {
1355 PrinterHolder printerHolder = mPrinterHolders.get(i);
1356 if (printerHolder.printer.getId().equals(printerId)) {
1357 // If already in the list - do nothing.
1358 if (i < getCount() - 2) {
1359 return;
1360 }
1361 // Else replace the last one (two items are not printers).
1362 final int lastPrinterIndex = getCount() - 3;
1363 mPrinterHolders.set(i, mPrinterHolders.get(lastPrinterIndex));
1364 mPrinterHolders.set(lastPrinterIndex, printerHolder);
1365 notifyDataSetChanged();
1366 return;
1367 }
1368 }
1369 }
1370
1371 @Override
1372 public int getCount() {
1373 return Math.min(mPrinterHolders.size() + 2, DEST_ADAPTER_MAX_ITEM_COUNT);
1374 }
1375
1376 @Override
1377 public boolean isEnabled(int position) {
1378 Object item = getItem(position);
1379 if (item instanceof PrinterHolder) {
1380 PrinterHolder printerHolder = (PrinterHolder) item;
1381 return !printerHolder.removed
1382 && printerHolder.printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
1383 }
1384 return true;
1385 }
1386
1387 @Override
1388 public Object getItem(int position) {
1389 if (mPrinterHolders.isEmpty()) {
1390 if (position == 0) {
1391 return mFakePdfPrinterHolder;
1392 }
1393 } else {
1394 if (position < 1) {
1395 return mPrinterHolders.get(position);
1396 }
1397 if (position == 1) {
1398 return mFakePdfPrinterHolder;
1399 }
1400 if (position < getCount() - 1) {
1401 return mPrinterHolders.get(position - 1);
1402 }
1403 }
1404 return null;
1405 }
1406
1407 @Override
1408 public long getItemId(int position) {
1409 if (mPrinterHolders.isEmpty()) {
1410 if (position == 0) {
1411 return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
1412 } else if (position == 1) {
1413 return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS;
1414 }
1415 } else {
1416 if (position == 1) {
1417 return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
1418 }
1419 if (position == getCount() - 1) {
1420 return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS;
1421 }
1422 }
1423 return position;
1424 }
1425
1426 @Override
1427 public View getDropDownView(int position, View convertView, ViewGroup parent) {
1428 View view = getView(position, convertView, parent);
1429 view.setEnabled(isEnabled(position));
1430 return view;
1431 }
1432
1433 @Override
1434 public View getView(int position, View convertView, ViewGroup parent) {
1435 if (convertView == null) {
1436 convertView = getLayoutInflater().inflate(
1437 R.layout.printer_dropdown_item, parent, false);
1438 }
1439
1440 CharSequence title = null;
1441 CharSequence subtitle = null;
1442 Drawable icon = null;
1443
1444 if (mPrinterHolders.isEmpty()) {
1445 if (position == 0 && getPdfPrinter() != null) {
1446 PrinterHolder printerHolder = (PrinterHolder) getItem(position);
1447 title = printerHolder.printer.getName();
1448 icon = getResources().getDrawable(com.android.internal.R.drawable.ic_menu_save);
1449 } else if (position == 1) {
1450 title = getString(R.string.all_printers);
1451 }
1452 } else {
1453 if (position == 1 && getPdfPrinter() != null) {
1454 PrinterHolder printerHolder = (PrinterHolder) getItem(position);
1455 title = printerHolder.printer.getName();
1456 icon = getResources().getDrawable(com.android.internal.R.drawable.ic_menu_save);
1457 } else if (position == getCount() - 1) {
1458 title = getString(R.string.all_printers);
1459 } else {
1460 PrinterHolder printerHolder = (PrinterHolder) getItem(position);
1461 title = printerHolder.printer.getName();
1462 try {
1463 PackageInfo packageInfo = getPackageManager().getPackageInfo(
1464 printerHolder.printer.getId().getServiceName().getPackageName(), 0);
1465 subtitle = packageInfo.applicationInfo.loadLabel(getPackageManager());
1466 icon = packageInfo.applicationInfo.loadIcon(getPackageManager());
1467 } catch (NameNotFoundException nnfe) {
1468 /* ignore */
1469 }
1470 }
1471 }
1472
1473 TextView titleView = (TextView) convertView.findViewById(R.id.title);
1474 titleView.setText(title);
1475
1476 TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle);
1477 if (!TextUtils.isEmpty(subtitle)) {
1478 subtitleView.setText(subtitle);
1479 subtitleView.setVisibility(View.VISIBLE);
1480 } else {
1481 subtitleView.setText(null);
1482 subtitleView.setVisibility(View.GONE);
1483 }
1484
1485 ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
1486 if (icon != null) {
1487 iconView.setImageDrawable(icon);
1488 iconView.setVisibility(View.VISIBLE);
1489 } else {
1490 iconView.setVisibility(View.INVISIBLE);
1491 }
1492
1493 return convertView;
1494 }
1495
1496 @Override
1497 public void onPrintersChanged(List<PrinterInfo> printers) {
1498 // We rearrange the printers if the user selects a printer
1499 // not shown in the initial short list. Therefore, we have
1500 // to keep the printer order.
1501
1502 // No old printers - do not bother keeping their position.
1503 if (mPrinterHolders.isEmpty()) {
1504 addPrinters(mPrinterHolders, printers);
1505 notifyDataSetChanged();
1506 return;
1507 }
1508
1509 // Add the new printers to a map.
1510 ArrayMap<PrinterId, PrinterInfo> newPrintersMap = new ArrayMap<>();
1511 final int printerCount = printers.size();
1512 for (int i = 0; i < printerCount; i++) {
1513 PrinterInfo printer = printers.get(i);
1514 newPrintersMap.put(printer.getId(), printer);
1515 }
1516
1517 List<PrinterHolder> newPrinterHolders = new ArrayList<>();
1518
1519 // Update printers we already have which are either updated or removed.
1520 // We do not remove printers if the currently selected printer is removed
1521 // to prevent the user printing to a wrong printer.
1522 final int oldPrinterCount = mPrinterHolders.size();
1523 for (int i = 0; i < oldPrinterCount; i++) {
1524 PrinterHolder printerHolder = mPrinterHolders.get(i);
1525 PrinterId oldPrinterId = printerHolder.printer.getId();
1526 PrinterInfo updatedPrinter = newPrintersMap.remove(oldPrinterId);
1527 if (updatedPrinter != null) {
1528 printerHolder.printer = updatedPrinter;
1529 } else {
1530 printerHolder.removed = true;
1531 }
1532 newPrinterHolders.add(printerHolder);
1533 }
1534
1535 // Add the rest of the new printers, i.e. what is left.
1536 addPrinters(newPrinterHolders, newPrintersMap.values());
1537
1538 mPrinterHolders.clear();
1539 mPrinterHolders.addAll(newPrinterHolders);
1540
1541 notifyDataSetChanged();
1542 }
1543
1544 @Override
1545 public void onPrintersInvalid() {
1546 mPrinterHolders.clear();
1547 notifyDataSetInvalidated();
1548 }
1549
1550 public PrinterHolder getPrinterHolder(PrinterId printerId) {
1551 final int itemCount = getCount();
1552 for (int i = 0; i < itemCount; i++) {
1553 Object item = getItem(i);
1554 if (item instanceof PrinterHolder) {
1555 PrinterHolder printerHolder = (PrinterHolder) item;
1556 if (printerId.equals(printerHolder.printer.getId())) {
1557 return printerHolder;
1558 }
1559 }
1560 }
1561 return null;
1562 }
1563
1564 public void pruneRemovedPrinters() {
1565 final int holderCounts = mPrinterHolders.size();
1566 for (int i = holderCounts - 1; i >= 0; i--) {
1567 PrinterHolder printerHolder = mPrinterHolders.get(i);
1568 if (printerHolder.removed) {
1569 mPrinterHolders.remove(i);
1570 }
1571 }
1572 }
1573
1574 private void addPrinters(List<PrinterHolder> list, Collection<PrinterInfo> printers) {
1575 for (PrinterInfo printer : printers) {
1576 PrinterHolder printerHolder = new PrinterHolder(printer);
1577 list.add(printerHolder);
1578 }
1579 }
1580
1581 private PrinterInfo createFakePdfPrinter() {
1582 MediaSize defaultMediaSize = MediaSizeUtils.getDefault(PrintActivity.this);
1583
1584 PrinterId printerId = new PrinterId(getComponentName(), "PDF printer");
1585
1586 PrinterCapabilitiesInfo.Builder builder =
1587 new PrinterCapabilitiesInfo.Builder(printerId);
1588
1589 String[] mediaSizeIds = getResources().getStringArray(R.array.pdf_printer_media_sizes);
1590 final int mediaSizeIdCount = mediaSizeIds.length;
1591 for (int i = 0; i < mediaSizeIdCount; i++) {
1592 String id = mediaSizeIds[i];
1593 MediaSize mediaSize = MediaSize.getStandardMediaSizeById(id);
1594 builder.addMediaSize(mediaSize, mediaSize.equals(defaultMediaSize));
1595 }
1596
1597 builder.addResolution(new Resolution("PDF resolution", "PDF resolution", 300, 300),
1598 true);
1599 builder.setColorModes(PrintAttributes.COLOR_MODE_COLOR
1600 | PrintAttributes.COLOR_MODE_MONOCHROME, PrintAttributes.COLOR_MODE_COLOR);
1601
1602 return new PrinterInfo.Builder(printerId, getString(R.string.save_as_pdf),
1603 PrinterInfo.STATUS_IDLE).setCapabilities(builder.build()).build();
1604 }
1605 }
1606
1607 private final class PrintersObserver extends DataSetObserver {
1608 @Override
1609 public void onChanged() {
1610 PrinterInfo oldPrinterState = mOldCurrentPrinter;
1611 if (oldPrinterState == null) {
1612 return;
1613 }
1614
1615 PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
1616 oldPrinterState.getId());
1617 if (printerHolder == null) {
1618 return;
1619 }
1620 PrinterInfo newPrinterState = printerHolder.printer;
1621
1622 if (!printerHolder.removed) {
1623 mDestinationSpinnerAdapter.pruneRemovedPrinters();
1624 } else {
1625 onPrinterUnavailable(newPrinterState);
1626 }
1627
1628 if (oldPrinterState.equals(newPrinterState)) {
1629 return;
1630 }
1631
1632 PrinterCapabilitiesInfo oldCapab = oldPrinterState.getCapabilities();
1633 PrinterCapabilitiesInfo newCapab = newPrinterState.getCapabilities();
1634
1635 final boolean hasCapab = newCapab != null;
1636 final boolean gotCapab = oldCapab == null && newCapab != null;
1637 final boolean lostCapab = oldCapab != null && newCapab == null;
1638 final boolean capabChanged = capabilitiesChanged(oldCapab, newCapab);
1639
1640 final int oldStatus = oldPrinterState.getStatus();
1641 final int newStatus = newPrinterState.getStatus();
1642
1643 final boolean isActive = newStatus != PrinterInfo.STATUS_UNAVAILABLE;
1644 final boolean becameActive = (oldStatus == PrinterInfo.STATUS_UNAVAILABLE
1645 && oldStatus != newStatus);
1646 final boolean becameInactive = (newStatus == PrinterInfo.STATUS_UNAVAILABLE
1647 && oldStatus != newStatus);
1648
1649 mPrinterAvailabilityDetector.updatePrinter(newPrinterState);
1650
1651 oldPrinterState.copyFrom(newPrinterState);
1652
1653 if ((isActive && gotCapab) || (becameActive && hasCapab)) {
1654 onPrinterAvailable(newPrinterState);
1655 } else if ((becameInactive && hasCapab)|| (isActive && lostCapab)) {
1656 onPrinterUnavailable(newPrinterState);
1657 }
1658
1659 if (hasCapab && capabChanged) {
1660 updatePrintAttributesFromCapabilities(newCapab);
1661 }
1662
1663 final boolean updateNeeded = ((capabChanged && hasCapab && isActive)
1664 || (becameActive && hasCapab) || (isActive && gotCapab));
1665
1666 if (updateNeeded && canUpdateDocument()) {
1667 updateDocument(true, false);
1668 }
1669
1670 updateOptionsUi();
1671 }
1672
1673 private boolean capabilitiesChanged(PrinterCapabilitiesInfo oldCapabilities,
1674 PrinterCapabilitiesInfo newCapabilities) {
1675 if (oldCapabilities == null) {
1676 if (newCapabilities != null) {
1677 return true;
1678 }
1679 } else if (!oldCapabilities.equals(newCapabilities)) {
1680 return true;
1681 }
1682 return false;
1683 }
1684 }
1685
1686 private final class MyOnItemSelectedListener implements AdapterView.OnItemSelectedListener {
1687 @Override
1688 public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) {
1689 if (spinner == mDestinationSpinner) {
1690 if (position == AdapterView.INVALID_POSITION) {
1691 return;
1692 }
1693
1694 if (id == DEST_ADAPTER_ITEM_ID_ALL_PRINTERS) {
1695 startSelectPrinterActivity();
1696 return;
1697 }
1698
1699 PrinterInfo currentPrinter = getCurrentPrinter();
1700
1701 // Why on earth item selected is called if no selection changed.
1702 if (mOldCurrentPrinter == currentPrinter) {
1703 return;
1704 }
1705 mOldCurrentPrinter = currentPrinter;
1706
1707 PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
1708 currentPrinter.getId());
1709 if (!printerHolder.removed) {
1710 mDestinationSpinnerAdapter.pruneRemovedPrinters();
1711 ensurePreviewUiShown();
1712 }
1713
1714 mPrintJob.setPrinterId(currentPrinter.getId());
1715 mPrintJob.setPrinterName(currentPrinter.getName());
1716
1717 mPrinterRegistry.setTrackedPrinter(currentPrinter.getId());
1718
1719 PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities();
1720 if (capabilities != null) {
1721 updatePrintAttributesFromCapabilities(capabilities);
1722 }
1723
1724 mPrinterAvailabilityDetector.updatePrinter(currentPrinter);
1725 } else if (spinner == mMediaSizeSpinner) {
1726 SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position);
1727 if (mOrientationSpinner.getSelectedItemPosition() == 0) {
1728 mPrintJob.getAttributes().setMediaSize(mediaItem.value.asPortrait());
1729 } else {
1730 mPrintJob.getAttributes().setMediaSize(mediaItem.value.asLandscape());
1731 }
1732 } else if (spinner == mColorModeSpinner) {
1733 SpinnerItem<Integer> colorModeItem = mColorModeSpinnerAdapter.getItem(position);
1734 mPrintJob.getAttributes().setColorMode(colorModeItem.value);
1735 } else if (spinner == mOrientationSpinner) {
1736 SpinnerItem<Integer> orientationItem = mOrientationSpinnerAdapter.getItem(position);
1737 PrintAttributes attributes = mPrintJob.getAttributes();
1738 if (orientationItem.value == ORIENTATION_PORTRAIT) {
1739 attributes.copyFrom(attributes.asPortrait());
1740 } else {
1741 attributes.copyFrom(attributes.asLandscape());
1742 }
1743 }
1744
1745 if (canUpdateDocument()) {
1746 updateDocument(true, false);
1747 }
1748
1749 updateOptionsUi();
1750 }
1751
1752 @Override
1753 public void onNothingSelected(AdapterView<?> parent) {
1754 /* do nothing*/
1755 }
1756 }
1757
1758 private boolean canUpdateDocument() {
1759 if (mPrintedDocument.isDestroyed()) {
1760 return false;
1761 }
1762
1763 if (hasErrors()) {
1764 return false;
1765 }
1766
1767 PrintAttributes attributes = mPrintJob.getAttributes();
1768
1769 final int colorMode = attributes.getColorMode();
1770 if (colorMode != PrintAttributes.COLOR_MODE_COLOR
1771 && colorMode != PrintAttributes.COLOR_MODE_MONOCHROME) {
1772 return false;
1773 }
1774 if (attributes.getMediaSize() == null) {
1775 return false;
1776 }
1777 if (attributes.getMinMargins() == null) {
1778 return false;
1779 }
1780 if (attributes.getResolution() == null) {
1781 return false;
1782 }
1783
1784 PrinterInfo currentPrinter = getCurrentPrinter();
1785 if (currentPrinter == null) {
1786 return false;
1787 }
1788 PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities();
1789 if (capabilities == null) {
1790 return false;
1791 }
1792 if (currentPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE) {
1793 return false;
1794 }
1795
1796 return true;
1797 }
1798
1799 private final class SelectAllOnFocusListener implements OnFocusChangeListener {
1800 @Override
1801 public void onFocusChange(View view, boolean hasFocus) {
1802 EditText editText = (EditText) view;
1803 if (!TextUtils.isEmpty(editText.getText())) {
1804 editText.setSelection(editText.getText().length());
1805 }
1806 }
1807 }
1808
1809 private final class RangeTextWatcher implements TextWatcher {
1810 @Override
1811 public void onTextChanged(CharSequence s, int start, int before, int count) {
1812 /* do nothing */
1813 }
1814
1815 @Override
1816 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1817 /* do nothing */
1818 }
1819
1820 @Override
1821 public void afterTextChanged(Editable editable) {
1822 final boolean hadErrors = hasErrors();
1823
1824 String text = editable.toString();
1825
1826 if (TextUtils.isEmpty(text)) {
1827 mPageRangeEditText.setError("");
1828 updateOptionsUi();
1829 return;
1830 }
1831
1832 String escapedText = PATTERN_ESCAPE_SPECIAL_CHARS.matcher(text).replaceAll("////");
1833 if (!PATTERN_PAGE_RANGE.matcher(escapedText).matches()) {
1834 mPageRangeEditText.setError("");
1835 updateOptionsUi();
1836 return;
1837 }
1838
1839 PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
1840 final int pageCount = (info != null) ? info.getPageCount() : 0;
1841
1842 // The range
1843 Matcher matcher = PATTERN_DIGITS.matcher(text);
1844 while (matcher.find()) {
1845 String numericString = text.substring(matcher.start(), matcher.end()).trim();
1846 if (TextUtils.isEmpty(numericString)) {
1847 continue;
1848 }
1849 final int pageIndex = Integer.parseInt(numericString);
1850 if (pageIndex < 1 || pageIndex > pageCount) {
1851 mPageRangeEditText.setError("");
1852 updateOptionsUi();
1853 return;
1854 }
1855 }
1856
1857 // We intentionally do not catch the case of the from page being
1858 // greater than the to page. When computing the requested pages
1859 // we just swap them if necessary.
1860
1861 // Keep the print job up to date with the selected pages if we
1862 // know how many pages are there in the document.
1863 mRequestedPages = computeRequestedPages();
1864
1865 mPageRangeEditText.setError(null);
1866 mPrintButton.setEnabled(true);
1867 updateOptionsUi();
1868
1869 if (hadErrors && !hasErrors()) {
1870 updateOptionsUi();
1871 }
1872 }
1873 }
1874
1875 private final class EditTextWatcher implements TextWatcher {
1876 @Override
1877 public void onTextChanged(CharSequence s, int start, int before, int count) {
1878 /* do nothing */
1879 }
1880
1881 @Override
1882 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1883 /* do nothing */
1884 }
1885
1886 @Override
1887 public void afterTextChanged(Editable editable) {
1888 final boolean hadErrors = hasErrors();
1889
1890 if (editable.length() == 0) {
1891 mCopiesEditText.setError("");
1892 updateOptionsUi();
1893 return;
1894 }
1895
1896 int copies = 0;
1897 try {
1898 copies = Integer.parseInt(editable.toString());
1899 } catch (NumberFormatException nfe) {
1900 /* ignore */
1901 }
1902
1903 if (copies < MIN_COPIES) {
1904 mCopiesEditText.setError("");
1905 updateOptionsUi();
1906 return;
1907 }
1908
1909 mPrintJob.setCopies(copies);
1910
1911 mCopiesEditText.setError(null);
1912
1913 updateOptionsUi();
1914
1915 if (hadErrors && canUpdateDocument()) {
1916 updateDocument(true, false);
1917 }
1918 }
1919 }
1920
1921 private final class ProgressMessageController implements Runnable {
1922 private static final long PROGRESS_TIMEOUT_MILLIS = 1000;
1923
1924 private final Handler mHandler;
1925
1926 private boolean mPosted;
1927
1928 public ProgressMessageController(Context context) {
1929 mHandler = new Handler(context.getMainLooper(), null, false);
1930 }
1931
1932 public void post() {
1933 if (mPosted) {
1934 return;
1935 }
1936 mPosted = true;
1937 mHandler.postDelayed(this, PROGRESS_TIMEOUT_MILLIS);
1938 }
1939
1940 public void cancel() {
1941 if (!mPosted) {
1942 return;
1943 }
1944 mPosted = false;
1945 mHandler.removeCallbacks(this);
1946 }
1947
1948 @Override
1949 public void run() {
1950 ensureProgressUiShown();
1951 }
1952 }
1953}