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