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