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