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