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