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