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