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