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