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