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