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