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