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