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