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