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