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