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