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