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