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