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