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