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