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