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