1 /* 2 * Copyright (C) 2013 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; 18 19 import android.app.Activity; 20 import android.app.Dialog; 21 import android.app.LoaderManager; 22 import android.content.ActivityNotFoundException; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.Loader; 27 import android.content.ServiceConnection; 28 import android.content.pm.PackageInfo; 29 import android.content.pm.PackageManager.NameNotFoundException; 30 import android.content.pm.ResolveInfo; 31 import android.content.pm.ServiceInfo; 32 import android.database.DataSetObserver; 33 import android.graphics.Rect; 34 import android.graphics.drawable.Drawable; 35 import android.net.Uri; 36 import android.os.AsyncTask; 37 import android.os.Bundle; 38 import android.os.Handler; 39 import android.os.IBinder; 40 import android.os.IBinder.DeathRecipient; 41 import android.os.Looper; 42 import android.os.Message; 43 import android.os.RemoteException; 44 import android.print.ILayoutResultCallback; 45 import android.print.IPrintDocumentAdapter; 46 import android.print.IPrintDocumentAdapterObserver; 47 import android.print.IWriteResultCallback; 48 import android.print.PageRange; 49 import android.print.PrintAttributes; 50 import android.print.PrintAttributes.Margins; 51 import android.print.PrintAttributes.MediaSize; 52 import android.print.PrintAttributes.Resolution; 53 import android.print.PrintDocumentAdapter; 54 import android.print.PrintDocumentInfo; 55 import android.print.PrintJobId; 56 import android.print.PrintJobInfo; 57 import android.print.PrintManager; 58 import android.print.PrinterCapabilitiesInfo; 59 import android.print.PrinterId; 60 import android.print.PrinterInfo; 61 import android.printservice.PrintService; 62 import android.printservice.PrintServiceInfo; 63 import android.provider.DocumentsContract; 64 import android.text.Editable; 65 import android.text.TextUtils; 66 import android.text.TextUtils.SimpleStringSplitter; 67 import android.text.TextWatcher; 68 import android.util.ArrayMap; 69 import android.util.AttributeSet; 70 import android.util.Log; 71 import android.view.Gravity; 72 import android.view.KeyEvent; 73 import android.view.MotionEvent; 74 import android.view.View; 75 import android.view.View.MeasureSpec; 76 import android.view.View.OnAttachStateChangeListener; 77 import android.view.View.OnClickListener; 78 import android.view.View.OnFocusChangeListener; 79 import android.view.ViewConfiguration; 80 import android.view.ViewGroup; 81 import android.view.ViewGroup.LayoutParams; 82 import android.view.ViewPropertyAnimator; 83 import android.view.inputmethod.InputMethodManager; 84 import android.widget.AdapterView; 85 import android.widget.AdapterView.OnItemSelectedListener; 86 import android.widget.ArrayAdapter; 87 import android.widget.BaseAdapter; 88 import android.widget.Button; 89 import android.widget.EditText; 90 import android.widget.FrameLayout; 91 import android.widget.ImageView; 92 import android.widget.Spinner; 93 import android.widget.TextView; 94 95 import com.android.printspooler.MediaSizeUtils.MediaSizeComparator; 96 97 import libcore.io.IoUtils; 98 99 import java.io.File; 100 import java.io.FileInputStream; 101 import java.io.FileNotFoundException; 102 import java.io.IOException; 103 import java.io.InputStream; 104 import java.io.OutputStream; 105 import java.lang.ref.WeakReference; 106 import java.util.ArrayList; 107 import java.util.Arrays; 108 import java.util.Collections; 109 import java.util.Comparator; 110 import java.util.List; 111 import java.util.concurrent.atomic.AtomicInteger; 112 import java.util.regex.Matcher; 113 import java.util.regex.Pattern; 114 115 /** 116 * Activity for configuring a print job. 117 */ 118 public class PrintJobConfigActivity extends Activity { 119 120 private static final String LOG_TAG = "PrintJobConfigActivity"; 121 122 private static final boolean DEBUG = false; 123 124 public static final String INTENT_EXTRA_PRINTER_ID = "INTENT_EXTRA_PRINTER_ID"; 125 126 private static final int LOADER_ID_PRINTERS_LOADER = 1; 127 128 private static final int ORIENTATION_PORTRAIT = 0; 129 private static final int ORIENTATION_LANDSCAPE = 1; 130 131 private static final int DEST_ADAPTER_MAX_ITEM_COUNT = 9; 132 133 private static final int DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF = Integer.MAX_VALUE; 134 private static final int DEST_ADAPTER_ITEM_ID_ALL_PRINTERS = Integer.MAX_VALUE - 1; 135 136 private static final int ACTIVITY_REQUEST_CREATE_FILE = 1; 137 private static final int ACTIVITY_REQUEST_SELECT_PRINTER = 2; 138 private static final int ACTIVITY_POPULATE_ADVANCED_PRINT_OPTIONS = 3; 139 140 private static final int CONTROLLER_STATE_FINISHED = 1; 141 private static final int CONTROLLER_STATE_FAILED = 2; 142 private static final int CONTROLLER_STATE_CANCELLED = 3; 143 private static final int CONTROLLER_STATE_INITIALIZED = 4; 144 private static final int CONTROLLER_STATE_STARTED = 5; 145 private static final int CONTROLLER_STATE_LAYOUT_STARTED = 6; 146 private static final int CONTROLLER_STATE_LAYOUT_COMPLETED = 7; 147 private static final int CONTROLLER_STATE_WRITE_STARTED = 8; 148 private static final int CONTROLLER_STATE_WRITE_COMPLETED = 9; 149 150 private static final int EDITOR_STATE_INITIALIZED = 1; 151 private static final int EDITOR_STATE_CONFIRMED_PRINT = 2; 152 private static final int EDITOR_STATE_CANCELLED = 3; 153 154 private static final int MIN_COPIES = 1; 155 private static final String MIN_COPIES_STRING = String.valueOf(MIN_COPIES); 156 157 private static final Pattern PATTERN_DIGITS = Pattern.compile("[\\d]+"); 158 159 private static final Pattern PATTERN_ESCAPE_SPECIAL_CHARS = Pattern.compile( 160 "(?=[]\\[+&|!(){}^\"~*?:\\\\])"); 161 162 private static final Pattern PATTERN_PAGE_RANGE = Pattern.compile( 163 "[\\s]*[0-9]*[\\s]*[\\-]?[\\s]*[0-9]*[\\s]*?(([,])" 164 + "[\\s]*[0-9]*[\\s]*[\\-]?[\\s]*[0-9]*[\\s]*|[\\s]*)+"); 165 166 public static final PageRange[] ALL_PAGES_ARRAY = new PageRange[] {PageRange.ALL_PAGES}; 167 168 private final PrintAttributes mOldPrintAttributes = new PrintAttributes.Builder().build(); 169 private final PrintAttributes mCurrPrintAttributes = new PrintAttributes.Builder().build(); 170 171 private final DeathRecipient mDeathRecipient = new DeathRecipient() { 172 @Override 173 public void binderDied() { 174 finish(); 175 } 176 }; 177 178 private Editor mEditor; 179 private Document mDocument; 180 private PrintController mController; 181 182 private PrintJobId mPrintJobId; 183 184 private IBinder mIPrintDocumentAdapter; 185 186 private Dialog mGeneratingPrintJobDialog; 187 188 private PrintSpoolerProvider mSpoolerProvider; 189 190 private String mCallingPackageName; 191 192 @Override 193 protected void onCreate(Bundle bundle) { 194 super.onCreate(bundle); 195 196 setTitle(R.string.print_dialog); 197 198 Bundle extras = getIntent().getExtras(); 199 200 PrintJobInfo printJob = extras.getParcelable(PrintManager.EXTRA_PRINT_JOB); 201 if (printJob == null) { 202 throw new IllegalArgumentException("printJob cannot be null"); 203 } 204 205 mPrintJobId = printJob.getId(); 206 mIPrintDocumentAdapter = extras.getBinder(PrintManager.EXTRA_PRINT_DOCUMENT_ADAPTER); 207 if (mIPrintDocumentAdapter == null) { 208 throw new IllegalArgumentException("PrintDocumentAdapter cannot be null"); 209 } 210 211 try { 212 IPrintDocumentAdapter.Stub.asInterface(mIPrintDocumentAdapter) 213 .setObserver(new PrintDocumentAdapterObserver(this)); 214 } catch (RemoteException re) { 215 finish(); 216 return; 217 } 218 219 PrintAttributes attributes = printJob.getAttributes(); 220 if (attributes != null) { 221 mCurrPrintAttributes.copyFrom(attributes); 222 } 223 224 mCallingPackageName = extras.getString(DocumentsContract.EXTRA_PACKAGE_NAME); 225 226 setContentView(R.layout.print_job_config_activity_container); 227 228 try { 229 mIPrintDocumentAdapter.linkToDeath(mDeathRecipient, 0); 230 } catch (RemoteException re) { 231 finish(); 232 return; 233 } 234 235 mDocument = new Document(); 236 mEditor = new Editor(); 237 238 mSpoolerProvider = new PrintSpoolerProvider(this, 239 new Runnable() { 240 @Override 241 public void run() { 242 // We got the spooler so unleash the UI. 243 mController = new PrintController(new RemotePrintDocumentAdapter( 244 IPrintDocumentAdapter.Stub.asInterface(mIPrintDocumentAdapter), 245 mSpoolerProvider.getSpooler().generateFileForPrintJob(mPrintJobId))); 246 mController.initialize(); 247 248 mEditor.initialize(); 249 mEditor.postCreate(); 250 } 251 }); 252 } 253 254 @Override 255 public void onResume() { 256 super.onResume(); 257 if (mSpoolerProvider.getSpooler() != null) { 258 mEditor.refreshCurrentPrinter(); 259 } 260 } 261 262 @Override 263 public void onPause() { 264 if (isFinishing()) { 265 if (mController != null && mController.hasStarted()) { 266 mController.finish(); 267 } 268 if (mEditor != null && mEditor.isPrintConfirmed() 269 && mController != null && mController.isFinished()) { 270 mSpoolerProvider.getSpooler().setPrintJobState(mPrintJobId, 271 PrintJobInfo.STATE_QUEUED, null); 272 } else { 273 mSpoolerProvider.getSpooler().setPrintJobState(mPrintJobId, 274 PrintJobInfo.STATE_CANCELED, null); 275 } 276 if (mGeneratingPrintJobDialog != null) { 277 mGeneratingPrintJobDialog.dismiss(); 278 mGeneratingPrintJobDialog = null; 279 } 280 mIPrintDocumentAdapter.unlinkToDeath(mDeathRecipient, 0); 281 mSpoolerProvider.destroy(); 282 } 283 super.onPause(); 284 } 285 286 public boolean onTouchEvent(MotionEvent event) { 287 if (mController != null && mEditor != null && 288 !mEditor.isPrintConfirmed() && mEditor.shouldCloseOnTouch(event)) { 289 if (!mController.isWorking()) { 290 PrintJobConfigActivity.this.finish(); 291 } 292 mEditor.cancel(); 293 return true; 294 } 295 return super.onTouchEvent(event); 296 } 297 298 public boolean onKeyDown(int keyCode, KeyEvent event) { 299 if (keyCode == KeyEvent.KEYCODE_BACK) { 300 event.startTracking(); 301 } 302 return super.onKeyDown(keyCode, event); 303 } 304 305 public boolean onKeyUp(int keyCode, KeyEvent event) { 306 if (mController != null && mEditor != null) { 307 if (keyCode == KeyEvent.KEYCODE_BACK) { 308 if (mEditor.isShwoingGeneratingPrintJobUi()) { 309 return true; 310 } 311 if (event.isTracking() && !event.isCanceled()) { 312 if (!mController.isWorking()) { 313 PrintJobConfigActivity.this.finish(); 314 } 315 } 316 mEditor.cancel(); 317 return true; 318 } 319 } 320 return super.onKeyUp(keyCode, event); 321 } 322 323 private boolean printAttributesChanged() { 324 return !mOldPrintAttributes.equals(mCurrPrintAttributes); 325 } 326 327 private class PrintController { 328 private final AtomicInteger mRequestCounter = new AtomicInteger(); 329 330 private final RemotePrintDocumentAdapter mRemotePrintAdapter; 331 332 private final Bundle mMetadata; 333 334 private final ControllerHandler mHandler; 335 336 private final LayoutResultCallback mLayoutResultCallback; 337 338 private final WriteResultCallback mWriteResultCallback; 339 340 private int mControllerState = CONTROLLER_STATE_INITIALIZED; 341 342 private boolean mHasStarted; 343 344 private PageRange[] mRequestedPages; 345 346 public PrintController(RemotePrintDocumentAdapter adapter) { 347 mRemotePrintAdapter = adapter; 348 mMetadata = new Bundle(); 349 mHandler = new ControllerHandler(getMainLooper()); 350 mLayoutResultCallback = new LayoutResultCallback(mHandler); 351 mWriteResultCallback = new WriteResultCallback(mHandler); 352 } 353 354 public void initialize() { 355 mHasStarted = false; 356 mControllerState = CONTROLLER_STATE_INITIALIZED; 357 } 358 359 public void cancel() { 360 if (isWorking()) { 361 mRemotePrintAdapter.cancel(); 362 } 363 mControllerState = CONTROLLER_STATE_CANCELLED; 364 } 365 366 public boolean isCancelled() { 367 return (mControllerState == CONTROLLER_STATE_CANCELLED); 368 } 369 370 public boolean isFinished() { 371 return (mControllerState == CONTROLLER_STATE_FINISHED); 372 } 373 374 public boolean hasStarted() { 375 return mHasStarted; 376 } 377 378 public boolean hasPerformedLayout() { 379 return mControllerState >= CONTROLLER_STATE_LAYOUT_COMPLETED; 380 } 381 382 public boolean isPerformingLayout() { 383 return mControllerState == CONTROLLER_STATE_LAYOUT_STARTED; 384 } 385 386 public boolean isWorking() { 387 return mControllerState == CONTROLLER_STATE_LAYOUT_STARTED 388 || mControllerState == CONTROLLER_STATE_WRITE_STARTED; 389 } 390 391 public void start() { 392 mControllerState = CONTROLLER_STATE_STARTED; 393 mHasStarted = true; 394 mRemotePrintAdapter.start(); 395 } 396 397 public void update() { 398 if (!mController.hasStarted()) { 399 mController.start(); 400 } 401 402 // If the print attributes are the same and we are performing 403 // a layout, then we have to wait for it to completed which will 404 // trigger writing of the necessary pages. 405 final boolean printAttributesChanged = printAttributesChanged(); 406 if (!printAttributesChanged && isPerformingLayout()) { 407 return; 408 } 409 410 // If print is confirmed we always do a layout since the previous 411 // ones were for preview and this one is for printing. 412 if (!printAttributesChanged && !mEditor.isPrintConfirmed()) { 413 if (mDocument.info == null) { 414 // We are waiting for the result of a layout, so do nothing. 415 return; 416 } 417 // If the attributes didn't change and we have done a layout, then 418 // we do not do a layout but may have to ask the app to write some 419 // pages. Hence, pretend layout completed and nothing changed, so 420 // we handle writing as usual. 421 handleOnLayoutFinished(mDocument.info, false, mRequestCounter.get()); 422 } else { 423 mSpoolerProvider.getSpooler().setPrintJobAttributesNoPersistence( 424 mPrintJobId, mCurrPrintAttributes); 425 426 mMetadata.putBoolean(PrintDocumentAdapter.EXTRA_PRINT_PREVIEW, 427 !mEditor.isPrintConfirmed()); 428 429 mControllerState = CONTROLLER_STATE_LAYOUT_STARTED; 430 431 mRemotePrintAdapter.layout(mOldPrintAttributes, mCurrPrintAttributes, 432 mLayoutResultCallback, mMetadata, mRequestCounter.incrementAndGet()); 433 434 mOldPrintAttributes.copyFrom(mCurrPrintAttributes); 435 } 436 } 437 438 public void finish() { 439 mControllerState = CONTROLLER_STATE_FINISHED; 440 mRemotePrintAdapter.finish(); 441 } 442 443 private void handleOnLayoutFinished(PrintDocumentInfo info, 444 boolean layoutChanged, int sequence) { 445 if (mRequestCounter.get() != sequence) { 446 return; 447 } 448 449 if (isCancelled()) { 450 mEditor.updateUi(); 451 if (mEditor.isDone()) { 452 PrintJobConfigActivity.this.finish(); 453 } 454 return; 455 } 456 457 mControllerState = CONTROLLER_STATE_LAYOUT_COMPLETED; 458 459 // For layout purposes we care only whether the type or the page 460 // count changed. We still do not have the size since we did not 461 // call write. We use "layoutChanged" set by the application to 462 // know whether something else changed about the document. 463 final boolean infoChanged = !equalsIgnoreSize(info, mDocument.info); 464 // If the info changed, we update the document and the print job. 465 if (infoChanged) { 466 mDocument.info = info; 467 // Set the info. 468 mSpoolerProvider.getSpooler().setPrintJobPrintDocumentInfoNoPersistence( 469 mPrintJobId, info); 470 } 471 472 // If the document info or the layout changed, then 473 // drop the pages since we have to fetch them again. 474 if (infoChanged || layoutChanged) { 475 mDocument.pages = null; 476 mSpoolerProvider.getSpooler().setPrintJobPagesNoPersistence( 477 mPrintJobId, null); 478 } 479 480 // No pages means that the user selected an invalid range while we 481 // were doing a layout or the layout returned a document info for 482 // which the selected range is invalid. In such a case we do not 483 // write anything and wait for the user to fix the range which will 484 // trigger an update. 485 mRequestedPages = mEditor.getRequestedPages(); 486 if (mRequestedPages == null || mRequestedPages.length == 0) { 487 mEditor.updateUi(); 488 if (mEditor.isDone()) { 489 PrintJobConfigActivity.this.finish(); 490 } 491 return; 492 } else { 493 // If print is not confirmed we just ask for the first of the 494 // selected pages to emulate a behavior that shows preview 495 // increasing the chances that apps will implement the APIs 496 // correctly. 497 if (!mEditor.isPrintConfirmed()) { 498 if (ALL_PAGES_ARRAY.equals(mRequestedPages)) { 499 mRequestedPages = new PageRange[] {new PageRange(0, 0)}; 500 } else { 501 final int firstPage = mRequestedPages[0].getStart(); 502 mRequestedPages = new PageRange[] {new PageRange(firstPage, firstPage)}; 503 } 504 } 505 } 506 507 // If the info and the layout did not change and we already have 508 // the requested pages, then nothing else to do. 509 if (!infoChanged && !layoutChanged 510 && PageRangeUtils.contains(mDocument.pages, mRequestedPages)) { 511 // Nothing interesting changed and we have all requested pages. 512 // Then update the print jobs's pages as we will not do a write 513 // and we usually update the pages in the write complete callback. 514 updatePrintJobPages(mDocument.pages, mRequestedPages); 515 mEditor.updateUi(); 516 if (mEditor.isDone()) { 517 requestCreatePdfFileOrFinish(); 518 } 519 return; 520 } 521 522 mEditor.updateUi(); 523 524 // Request a write of the pages of interest. 525 mControllerState = CONTROLLER_STATE_WRITE_STARTED; 526 mRemotePrintAdapter.write(mRequestedPages, mWriteResultCallback, 527 mRequestCounter.incrementAndGet()); 528 } 529 530 private void handleOnLayoutFailed(final CharSequence error, int sequence) { 531 if (mRequestCounter.get() != sequence) { 532 return; 533 } 534 mControllerState = CONTROLLER_STATE_FAILED; 535 mEditor.showUi(Editor.UI_ERROR, new Runnable() { 536 @Override 537 public void run() { 538 if (!TextUtils.isEmpty(error)) { 539 TextView messageView = (TextView) findViewById(R.id.message); 540 messageView.setText(error); 541 } 542 } 543 }); 544 } 545 546 private void handleOnWriteFinished(PageRange[] pages, int sequence) { 547 if (mRequestCounter.get() != sequence) { 548 return; 549 } 550 551 if (isCancelled()) { 552 if (mEditor.isDone()) { 553 PrintJobConfigActivity.this.finish(); 554 } 555 return; 556 } 557 558 mControllerState = CONTROLLER_STATE_WRITE_COMPLETED; 559 560 // Update the document size. 561 File file = mSpoolerProvider.getSpooler() 562 .generateFileForPrintJob(mPrintJobId); 563 mDocument.info.setDataSize(file.length()); 564 565 // Update the print job with the updated info. 566 mSpoolerProvider.getSpooler().setPrintJobPrintDocumentInfoNoPersistence( 567 mPrintJobId, mDocument.info); 568 569 // Update which pages we have fetched. 570 mDocument.pages = PageRangeUtils.normalize(pages); 571 572 if (DEBUG) { 573 Log.i(LOG_TAG, "Requested: " + Arrays.toString(mRequestedPages) 574 + " and got: " + Arrays.toString(mDocument.pages)); 575 } 576 577 updatePrintJobPages(mDocument.pages, mRequestedPages); 578 579 if (mEditor.isDone()) { 580 requestCreatePdfFileOrFinish(); 581 } 582 } 583 584 private void updatePrintJobPages(PageRange[] writtenPages, PageRange[] requestedPages) { 585 // Adjust the print job pages based on what was requested and written. 586 // The cases are ordered in the most expected to the least expected. 587 if (Arrays.equals(writtenPages, requestedPages)) { 588 // We got a document with exactly the pages we wanted. Hence, 589 // the printer has to print all pages in the data. 590 mSpoolerProvider.getSpooler().setPrintJobPagesNoPersistence(mPrintJobId, 591 ALL_PAGES_ARRAY); 592 } else if (Arrays.equals(writtenPages, ALL_PAGES_ARRAY)) { 593 // We requested specific pages but got all of them. Hence, 594 // the printer has to print only the requested pages. 595 mSpoolerProvider.getSpooler().setPrintJobPagesNoPersistence(mPrintJobId, 596 requestedPages); 597 } else if (PageRangeUtils.contains(writtenPages, requestedPages)) { 598 // We requested specific pages and got more but not all pages. 599 // Hence, we have to offset appropriately the printed pages to 600 // be based off the start of the written ones instead of zero. 601 // The written pages are always non-null and not empty. 602 final int offset = -writtenPages[0].getStart(); 603 PageRange[] offsetPages = Arrays.copyOf(requestedPages, requestedPages.length); 604 PageRangeUtils.offset(offsetPages, offset); 605 mSpoolerProvider.getSpooler().setPrintJobPagesNoPersistence(mPrintJobId, 606 offsetPages); 607 } else if (Arrays.equals(requestedPages, ALL_PAGES_ARRAY) 608 && writtenPages.length == 1 && writtenPages[0].getStart() == 0 609 && writtenPages[0].getEnd() == mDocument.info.getPageCount() - 1) { 610 // We requested all pages via the special constant and got all 611 // of them as an explicit enumeration. Hence, the printer has 612 // to print only the requested pages. 613 mSpoolerProvider.getSpooler().setPrintJobPagesNoPersistence(mPrintJobId, 614 writtenPages); 615 } else { 616 // We did not get the pages we requested, then the application 617 // misbehaves, so we fail quickly. 618 mControllerState = CONTROLLER_STATE_FAILED; 619 Log.e(LOG_TAG, "Received invalid pages from the app"); 620 mEditor.showUi(Editor.UI_ERROR, null); 621 } 622 } 623 624 private void requestCreatePdfFileOrFinish() { 625 if (mEditor.isPrintingToPdf()) { 626 Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); 627 intent.setType("application/pdf"); 628 intent.putExtra(Intent.EXTRA_TITLE, mDocument.info.getName()); 629 intent.putExtra(DocumentsContract.EXTRA_PACKAGE_NAME, mCallingPackageName); 630 startActivityForResult(intent, ACTIVITY_REQUEST_CREATE_FILE); 631 } else { 632 PrintJobConfigActivity.this.finish(); 633 } 634 } 635 636 private void handleOnWriteFailed(final CharSequence error, int sequence) { 637 if (mRequestCounter.get() != sequence) { 638 return; 639 } 640 mControllerState = CONTROLLER_STATE_FAILED; 641 mEditor.showUi(Editor.UI_ERROR, new Runnable() { 642 @Override 643 public void run() { 644 if (!TextUtils.isEmpty(error)) { 645 TextView messageView = (TextView) findViewById(R.id.message); 646 messageView.setText(error); 647 } 648 } 649 }); 650 } 651 652 private boolean equalsIgnoreSize(PrintDocumentInfo lhs, PrintDocumentInfo rhs) { 653 if (lhs == rhs) { 654 return true; 655 } 656 if (lhs == null) { 657 if (rhs != null) { 658 return false; 659 } 660 } else { 661 if (rhs == null) { 662 return false; 663 } 664 if (lhs.getContentType() != rhs.getContentType() 665 || lhs.getPageCount() != rhs.getPageCount()) { 666 return false; 667 } 668 } 669 return true; 670 } 671 672 private final class ControllerHandler extends Handler { 673 public static final int MSG_ON_LAYOUT_FINISHED = 1; 674 public static final int MSG_ON_LAYOUT_FAILED = 2; 675 public static final int MSG_ON_WRITE_FINISHED = 3; 676 public static final int MSG_ON_WRITE_FAILED = 4; 677 678 public ControllerHandler(Looper looper) { 679 super(looper, null, false); 680 } 681 682 @Override 683 public void handleMessage(Message message) { 684 switch (message.what) { 685 case MSG_ON_LAYOUT_FINISHED: { 686 PrintDocumentInfo info = (PrintDocumentInfo) message.obj; 687 final boolean changed = (message.arg1 == 1); 688 final int sequence = message.arg2; 689 handleOnLayoutFinished(info, changed, sequence); 690 } break; 691 692 case MSG_ON_LAYOUT_FAILED: { 693 CharSequence error = (CharSequence) message.obj; 694 final int sequence = message.arg1; 695 handleOnLayoutFailed(error, sequence); 696 } break; 697 698 case MSG_ON_WRITE_FINISHED: { 699 PageRange[] pages = (PageRange[]) message.obj; 700 final int sequence = message.arg1; 701 handleOnWriteFinished(pages, sequence); 702 } break; 703 704 case MSG_ON_WRITE_FAILED: { 705 CharSequence error = (CharSequence) message.obj; 706 final int sequence = message.arg1; 707 handleOnWriteFailed(error, sequence); 708 } break; 709 } 710 } 711 } 712 } 713 714 private static final class LayoutResultCallback extends ILayoutResultCallback.Stub { 715 private final WeakReference<PrintController.ControllerHandler> mWeakHandler; 716 717 public LayoutResultCallback(PrintController.ControllerHandler handler) { 718 mWeakHandler = new WeakReference<PrintController.ControllerHandler>(handler); 719 } 720 721 @Override 722 public void onLayoutFinished(PrintDocumentInfo info, boolean changed, int sequence) { 723 Handler handler = mWeakHandler.get(); 724 if (handler != null) { 725 handler.obtainMessage(PrintController.ControllerHandler.MSG_ON_LAYOUT_FINISHED, 726 changed ? 1 : 0, sequence, info).sendToTarget(); 727 } 728 } 729 730 @Override 731 public void onLayoutFailed(CharSequence error, int sequence) { 732 Handler handler = mWeakHandler.get(); 733 if (handler != null) { 734 handler.obtainMessage(PrintController.ControllerHandler.MSG_ON_LAYOUT_FAILED, 735 sequence, 0, error).sendToTarget(); 736 } 737 } 738 } 739 740 private static final class WriteResultCallback extends IWriteResultCallback.Stub { 741 private final WeakReference<PrintController.ControllerHandler> mWeakHandler; 742 743 public WriteResultCallback(PrintController.ControllerHandler handler) { 744 mWeakHandler = new WeakReference<PrintController.ControllerHandler>(handler); 745 } 746 747 @Override 748 public void onWriteFinished(PageRange[] pages, int sequence) { 749 Handler handler = mWeakHandler.get(); 750 if (handler != null) { 751 handler.obtainMessage(PrintController.ControllerHandler.MSG_ON_WRITE_FINISHED, 752 sequence, 0, pages).sendToTarget(); 753 } 754 } 755 756 @Override 757 public void onWriteFailed(CharSequence error, int sequence) { 758 Handler handler = mWeakHandler.get(); 759 if (handler != null) { 760 handler.obtainMessage(PrintController.ControllerHandler.MSG_ON_WRITE_FAILED, 761 sequence, 0, error).sendToTarget(); 762 } 763 } 764 } 765 766 @Override 767 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 768 switch (requestCode) { 769 case ACTIVITY_REQUEST_CREATE_FILE: { 770 if (data != null) { 771 Uri uri = data.getData(); 772 writePrintJobDataAndFinish(uri); 773 } else { 774 mEditor.showUi(Editor.UI_EDITING_PRINT_JOB, 775 new Runnable() { 776 @Override 777 public void run() { 778 mEditor.initialize(); 779 mEditor.bindUi(); 780 mEditor.reselectCurrentPrinter(); 781 mEditor.updateUi(); 782 } 783 }); 784 } 785 } break; 786 787 case ACTIVITY_REQUEST_SELECT_PRINTER: { 788 if (resultCode == RESULT_OK) { 789 PrinterId printerId = (PrinterId) data.getParcelableExtra( 790 INTENT_EXTRA_PRINTER_ID); 791 if (printerId != null) { 792 mEditor.ensurePrinterSelected(printerId); 793 break; 794 } 795 } 796 mEditor.ensureCurrentPrinterSelected(); 797 } break; 798 799 case ACTIVITY_POPULATE_ADVANCED_PRINT_OPTIONS: { 800 if (resultCode == RESULT_OK) { 801 PrintJobInfo printJobInfo = (PrintJobInfo) data.getParcelableExtra( 802 PrintService.EXTRA_PRINT_JOB_INFO); 803 if (printJobInfo != null) { 804 mEditor.updateFromAdvancedOptions(printJobInfo); 805 break; 806 } 807 } 808 mEditor.cancel(); 809 PrintJobConfigActivity.this.finish(); 810 } break; 811 } 812 } 813 814 private void writePrintJobDataAndFinish(final Uri uri) { 815 new AsyncTask<Void, Void, Void>() { 816 @Override 817 protected Void doInBackground(Void... params) { 818 InputStream in = null; 819 OutputStream out = null; 820 try { 821 PrintJobInfo printJob = mSpoolerProvider.getSpooler() 822 .getPrintJobInfo(mPrintJobId, PrintManager.APP_ID_ANY); 823 if (printJob == null) { 824 return null; 825 } 826 File file = mSpoolerProvider.getSpooler() 827 .generateFileForPrintJob(mPrintJobId); 828 in = new FileInputStream(file); 829 out = getContentResolver().openOutputStream(uri); 830 final byte[] buffer = new byte[8192]; 831 while (true) { 832 final int readByteCount = in.read(buffer); 833 if (readByteCount < 0) { 834 break; 835 } 836 out.write(buffer, 0, readByteCount); 837 } 838 } catch (FileNotFoundException fnfe) { 839 Log.e(LOG_TAG, "Error writing print job data!", fnfe); 840 } catch (IOException ioe) { 841 Log.e(LOG_TAG, "Error writing print job data!", ioe); 842 } finally { 843 IoUtils.closeQuietly(in); 844 IoUtils.closeQuietly(out); 845 } 846 return null; 847 } 848 849 @Override 850 public void onPostExecute(Void result) { 851 mEditor.cancel(); 852 PrintJobConfigActivity.this.finish(); 853 } 854 }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); 855 } 856 857 private final class Editor { 858 private static final int UI_NONE = 0; 859 private static final int UI_EDITING_PRINT_JOB = 1; 860 private static final int UI_GENERATING_PRINT_JOB = 2; 861 private static final int UI_ERROR = 3; 862 863 private EditText mCopiesEditText; 864 865 private TextView mRangeOptionsTitle; 866 private TextView mPageRangeTitle; 867 private EditText mPageRangeEditText; 868 869 private Spinner mDestinationSpinner; 870 private DestinationAdapter mDestinationSpinnerAdapter; 871 872 private Spinner mMediaSizeSpinner; 873 private ArrayAdapter<SpinnerItem<MediaSize>> mMediaSizeSpinnerAdapter; 874 875 private Spinner mColorModeSpinner; 876 private ArrayAdapter<SpinnerItem<Integer>> mColorModeSpinnerAdapter; 877 878 private Spinner mOrientationSpinner; 879 private ArrayAdapter<SpinnerItem<Integer>> mOrientationSpinnerAdapter; 880 881 private Spinner mRangeOptionsSpinner; 882 private ArrayAdapter<SpinnerItem<Integer>> mRangeOptionsSpinnerAdapter; 883 884 private SimpleStringSplitter mStringCommaSplitter = 885 new SimpleStringSplitter(','); 886 887 private View mContentContainer; 888 889 private View mAdvancedPrintOptionsContainer; 890 891 private Button mAdvancedOptionsButton; 892 893 private Button mPrintButton; 894 895 private PrinterId mNextPrinterId; 896 897 private PrinterInfo mCurrentPrinter; 898 899 private MediaSizeComparator mMediaSizeComparator; 900 901 private final OnFocusChangeListener mFocusListener = new OnFocusChangeListener() { 902 @Override 903 public void onFocusChange(View view, boolean hasFocus) { 904 EditText editText = (EditText) view; 905 if (!TextUtils.isEmpty(editText.getText())) { 906 editText.setSelection(editText.getText().length()); 907 } 908 } 909 }; 910 911 private final OnItemSelectedListener mOnItemSelectedListener = 912 new AdapterView.OnItemSelectedListener() { 913 @Override 914 public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) { 915 if (spinner == mDestinationSpinner) { 916 if (mIgnoreNextDestinationChange) { 917 mIgnoreNextDestinationChange = false; 918 return; 919 } 920 921 if (position == AdapterView.INVALID_POSITION) { 922 updateUi(); 923 return; 924 } 925 926 if (id == DEST_ADAPTER_ITEM_ID_ALL_PRINTERS) { 927 startSelectPrinterActivity(); 928 return; 929 } 930 931 mCapabilitiesTimeout.remove(); 932 933 mCurrentPrinter = (PrinterInfo) mDestinationSpinnerAdapter 934 .getItem(position); 935 936 mSpoolerProvider.getSpooler().setPrintJobPrinterNoPersistence( 937 mPrintJobId, mCurrentPrinter); 938 939 if (mCurrentPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE) { 940 mCapabilitiesTimeout.post(); 941 updateUi(); 942 return; 943 } 944 945 PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities(); 946 if (capabilities == null) { 947 mCapabilitiesTimeout.post(); 948 updateUi(); 949 refreshCurrentPrinter(); 950 } else { 951 updatePrintAttributes(capabilities); 952 updateUi(); 953 mController.update(); 954 refreshCurrentPrinter(); 955 } 956 } else if (spinner == mMediaSizeSpinner) { 957 if (mIgnoreNextMediaSizeChange) { 958 mIgnoreNextMediaSizeChange = false; 959 return; 960 } 961 if (mOldMediaSizeSelectionIndex 962 == mMediaSizeSpinner.getSelectedItemPosition()) { 963 mOldMediaSizeSelectionIndex = AdapterView.INVALID_POSITION; 964 return; 965 } 966 SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position); 967 if (mOrientationSpinner.getSelectedItemPosition() == 0) { 968 mCurrPrintAttributes.setMediaSize(mediaItem.value.asPortrait()); 969 } else { 970 mCurrPrintAttributes.setMediaSize(mediaItem.value.asLandscape()); 971 } 972 if (!hasErrors()) { 973 mController.update(); 974 } 975 } else if (spinner == mColorModeSpinner) { 976 if (mIgnoreNextColorChange) { 977 mIgnoreNextColorChange = false; 978 return; 979 } 980 if (mOldColorModeSelectionIndex 981 == mColorModeSpinner.getSelectedItemPosition()) { 982 mOldColorModeSelectionIndex = AdapterView.INVALID_POSITION; 983 return; 984 } 985 SpinnerItem<Integer> colorModeItem = 986 mColorModeSpinnerAdapter.getItem(position); 987 mCurrPrintAttributes.setColorMode(colorModeItem.value); 988 if (!hasErrors()) { 989 mController.update(); 990 } 991 } else if (spinner == mOrientationSpinner) { 992 if (mIgnoreNextOrientationChange) { 993 mIgnoreNextOrientationChange = false; 994 return; 995 } 996 SpinnerItem<Integer> orientationItem = 997 mOrientationSpinnerAdapter.getItem(position); 998 setCurrentPrintAttributesOrientation(orientationItem.value); 999 if (!hasErrors()) { 1000 mController.update(); 1001 } 1002 } else if (spinner == mRangeOptionsSpinner) { 1003 if (mIgnoreNextRangeOptionChange) { 1004 mIgnoreNextRangeOptionChange = false; 1005 return; 1006 } 1007 updateUi(); 1008 if (!hasErrors()) { 1009 mController.update(); 1010 } 1011 } 1012 } 1013 1014 @Override 1015 public void onNothingSelected(AdapterView<?> parent) { 1016 /* do nothing*/ 1017 } 1018 }; 1019 1020 private void setCurrentPrintAttributesOrientation(int orientation) { 1021 MediaSize mediaSize = mCurrPrintAttributes.getMediaSize(); 1022 if (orientation == ORIENTATION_PORTRAIT) { 1023 if (!mediaSize.isPortrait()) { 1024 // Rotate the media size. 1025 mCurrPrintAttributes.setMediaSize(mediaSize.asPortrait()); 1026 1027 // Rotate the resolution. 1028 Resolution oldResolution = mCurrPrintAttributes.getResolution(); 1029 Resolution newResolution = new Resolution( 1030 oldResolution.getId(), 1031 oldResolution.getLabel(), 1032 oldResolution.getVerticalDpi(), 1033 oldResolution.getHorizontalDpi()); 1034 mCurrPrintAttributes.setResolution(newResolution); 1035 1036 // Rotate the physical margins. 1037 Margins oldMinMargins = mCurrPrintAttributes.getMinMargins(); 1038 Margins newMinMargins = new Margins( 1039 oldMinMargins.getBottomMils(), 1040 oldMinMargins.getLeftMils(), 1041 oldMinMargins.getTopMils(), 1042 oldMinMargins.getRightMils()); 1043 mCurrPrintAttributes.setMinMargins(newMinMargins); 1044 } 1045 } else { 1046 if (mediaSize.isPortrait()) { 1047 // Rotate the media size. 1048 mCurrPrintAttributes.setMediaSize(mediaSize.asLandscape()); 1049 1050 // Rotate the resolution. 1051 Resolution oldResolution = mCurrPrintAttributes.getResolution(); 1052 Resolution newResolution = new Resolution( 1053 oldResolution.getId(), 1054 oldResolution.getLabel(), 1055 oldResolution.getVerticalDpi(), 1056 oldResolution.getHorizontalDpi()); 1057 mCurrPrintAttributes.setResolution(newResolution); 1058 1059 // Rotate the physical margins. 1060 Margins oldMinMargins = mCurrPrintAttributes.getMinMargins(); 1061 Margins newMargins = new Margins( 1062 oldMinMargins.getTopMils(), 1063 oldMinMargins.getRightMils(), 1064 oldMinMargins.getBottomMils(), 1065 oldMinMargins.getLeftMils()); 1066 mCurrPrintAttributes.setMinMargins(newMargins); 1067 } 1068 } 1069 } 1070 1071 private void updatePrintAttributes(PrinterCapabilitiesInfo capabilities) { 1072 PrintAttributes defaults = capabilities.getDefaults(); 1073 1074 // Sort the media sizes based on the current locale. 1075 List<MediaSize> sortedMediaSizes = new ArrayList<MediaSize>( 1076 capabilities.getMediaSizes()); 1077 Collections.sort(sortedMediaSizes, mMediaSizeComparator); 1078 1079 // Media size. 1080 MediaSize currMediaSize = mCurrPrintAttributes.getMediaSize(); 1081 if (currMediaSize == null) { 1082 mCurrPrintAttributes.setMediaSize(defaults.getMediaSize()); 1083 } else { 1084 MediaSize currMediaSizePortrait = currMediaSize.asPortrait(); 1085 final int mediaSizeCount = sortedMediaSizes.size(); 1086 for (int i = 0; i < mediaSizeCount; i++) { 1087 MediaSize mediaSize = sortedMediaSizes.get(i); 1088 if (currMediaSizePortrait.equals(mediaSize.asPortrait())) { 1089 mCurrPrintAttributes.setMediaSize(currMediaSize); 1090 break; 1091 } 1092 } 1093 } 1094 1095 // Color mode. 1096 final int colorMode = mCurrPrintAttributes.getColorMode(); 1097 if ((capabilities.getColorModes() & colorMode) == 0) { 1098 mCurrPrintAttributes.setColorMode(colorMode); 1099 } 1100 1101 // Resolution 1102 Resolution resolution = mCurrPrintAttributes.getResolution(); 1103 if (resolution == null || !capabilities.getResolutions().contains(resolution)) { 1104 mCurrPrintAttributes.setResolution(defaults.getResolution()); 1105 } 1106 1107 // Margins. 1108 Margins margins = mCurrPrintAttributes.getMinMargins(); 1109 if (margins == null) { 1110 mCurrPrintAttributes.setMinMargins(defaults.getMinMargins()); 1111 } else { 1112 Margins minMargins = capabilities.getMinMargins(); 1113 if (margins.getLeftMils() < minMargins.getLeftMils() 1114 || margins.getTopMils() < minMargins.getTopMils() 1115 || margins.getRightMils() > minMargins.getRightMils() 1116 || margins.getBottomMils() > minMargins.getBottomMils()) { 1117 mCurrPrintAttributes.setMinMargins(defaults.getMinMargins()); 1118 } 1119 } 1120 } 1121 1122 private final TextWatcher mCopiesTextWatcher = new TextWatcher() { 1123 @Override 1124 public void onTextChanged(CharSequence s, int start, int before, int count) { 1125 /* do nothing */ 1126 } 1127 1128 @Override 1129 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 1130 /* do nothing */ 1131 } 1132 1133 @Override 1134 public void afterTextChanged(Editable editable) { 1135 if (mIgnoreNextCopiesChange) { 1136 mIgnoreNextCopiesChange = false; 1137 return; 1138 } 1139 1140 final boolean hadErrors = hasErrors(); 1141 1142 if (editable.length() == 0) { 1143 mCopiesEditText.setError(""); 1144 updateUi(); 1145 return; 1146 } 1147 1148 int copies = 0; 1149 try { 1150 copies = Integer.parseInt(editable.toString()); 1151 } catch (NumberFormatException nfe) { 1152 /* ignore */ 1153 } 1154 1155 if (copies < MIN_COPIES) { 1156 mCopiesEditText.setError(""); 1157 updateUi(); 1158 return; 1159 } 1160 1161 mCopiesEditText.setError(null); 1162 mSpoolerProvider.getSpooler().setPrintJobCopiesNoPersistence( 1163 mPrintJobId, copies); 1164 updateUi(); 1165 1166 if (hadErrors && !hasErrors() && printAttributesChanged()) { 1167 mController.update(); 1168 } 1169 } 1170 }; 1171 1172 private final TextWatcher mRangeTextWatcher = new TextWatcher() { 1173 @Override 1174 public void onTextChanged(CharSequence s, int start, int before, int count) { 1175 /* do nothing */ 1176 } 1177 1178 @Override 1179 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 1180 /* do nothing */ 1181 } 1182 1183 @Override 1184 public void afterTextChanged(Editable editable) { 1185 if (mIgnoreNextRangeChange) { 1186 mIgnoreNextRangeChange = false; 1187 return; 1188 } 1189 1190 final boolean hadErrors = hasErrors(); 1191 1192 String text = editable.toString(); 1193 1194 if (TextUtils.isEmpty(text)) { 1195 mPageRangeEditText.setError(""); 1196 updateUi(); 1197 return; 1198 } 1199 1200 String escapedText = PATTERN_ESCAPE_SPECIAL_CHARS.matcher(text).replaceAll("////"); 1201 if (!PATTERN_PAGE_RANGE.matcher(escapedText).matches()) { 1202 mPageRangeEditText.setError(""); 1203 updateUi(); 1204 return; 1205 } 1206 1207 // The range 1208 Matcher matcher = PATTERN_DIGITS.matcher(text); 1209 while (matcher.find()) { 1210 String numericString = text.substring(matcher.start(), matcher.end()).trim(); 1211 if (TextUtils.isEmpty(numericString)) { 1212 continue; 1213 } 1214 final int pageIndex = Integer.parseInt(numericString); 1215 if (pageIndex < 1 || pageIndex > mDocument.info.getPageCount()) { 1216 mPageRangeEditText.setError(""); 1217 updateUi(); 1218 return; 1219 } 1220 } 1221 1222 // We intentionally do not catch the case of the from page being 1223 // greater than the to page. When computing the requested pages 1224 // we just swap them if necessary. 1225 1226 // Keep the print job up to date with the selected pages if we 1227 // know how many pages are there in the document. 1228 PageRange[] requestedPages = getRequestedPages(); 1229 if (requestedPages != null && requestedPages.length > 0 1230 && requestedPages[requestedPages.length - 1].getEnd() 1231 < mDocument.info.getPageCount()) { 1232 mSpoolerProvider.getSpooler().setPrintJobPagesNoPersistence( 1233 mPrintJobId, requestedPages); 1234 } 1235 1236 mPageRangeEditText.setError(null); 1237 mPrintButton.setEnabled(true); 1238 updateUi(); 1239 1240 if (hadErrors && !hasErrors() && printAttributesChanged()) { 1241 updateUi(); 1242 } 1243 } 1244 }; 1245 1246 private final WaitForPrinterCapabilitiesTimeout mCapabilitiesTimeout = 1247 new WaitForPrinterCapabilitiesTimeout(); 1248 1249 private int mEditorState; 1250 1251 private boolean mIgnoreNextDestinationChange; 1252 private int mOldMediaSizeSelectionIndex; 1253 private int mOldColorModeSelectionIndex; 1254 private boolean mIgnoreNextOrientationChange; 1255 private boolean mIgnoreNextRangeOptionChange; 1256 private boolean mIgnoreNextCopiesChange; 1257 private boolean mIgnoreNextRangeChange; 1258 private boolean mIgnoreNextMediaSizeChange; 1259 private boolean mIgnoreNextColorChange; 1260 1261 private int mCurrentUi = UI_NONE; 1262 1263 private boolean mFavoritePrinterSelected; 1264 1265 public Editor() { 1266 showUi(UI_EDITING_PRINT_JOB, null); 1267 } 1268 1269 public void postCreate() { 1270 // Destination. 1271 mMediaSizeComparator = new MediaSizeComparator(PrintJobConfigActivity.this); 1272 mDestinationSpinnerAdapter = new DestinationAdapter(); 1273 mDestinationSpinnerAdapter.registerDataSetObserver(new DataSetObserver() { 1274 @Override 1275 public void onChanged() { 1276 // Initially, we have only safe to PDF as a printer but after some 1277 // printers are loaded we want to select the user's favorite one 1278 // which is the first. 1279 if (!mFavoritePrinterSelected && mDestinationSpinnerAdapter.getCount() > 2) { 1280 mFavoritePrinterSelected = true; 1281 mDestinationSpinner.setSelection(0); 1282 // Workaround again the weird spinner behavior to notify for selection 1283 // change on the next layout pass as the current printer is used below. 1284 mCurrentPrinter = (PrinterInfo) mDestinationSpinnerAdapter.getItem(0); 1285 } 1286 1287 // If there is a next printer to select and we succeed selecting 1288 // it - done. Let the selection handling code make everything right. 1289 if (mNextPrinterId != null && selectPrinter(mNextPrinterId)) { 1290 mNextPrinterId = null; 1291 return; 1292 } 1293 1294 // If the current printer properties changed, we update the UI. 1295 if (mCurrentPrinter != null) { 1296 final int printerCount = mDestinationSpinnerAdapter.getCount(); 1297 for (int i = 0; i < printerCount; i++) { 1298 Object item = mDestinationSpinnerAdapter.getItem(i); 1299 // Some items are not printers 1300 if (item instanceof PrinterInfo) { 1301 PrinterInfo printer = (PrinterInfo) item; 1302 if (!printer.getId().equals(mCurrentPrinter.getId())) { 1303 continue; 1304 } 1305 1306 // If nothing changed - done. 1307 if (mCurrentPrinter.equals(printer)) { 1308 return; 1309 } 1310 1311 // If the current printer became available and has no 1312 // capabilities, we refresh it. 1313 if (mCurrentPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE 1314 && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE 1315 && printer.getCapabilities() == null) { 1316 if (!mCapabilitiesTimeout.isPosted()) { 1317 mCapabilitiesTimeout.post(); 1318 } 1319 mCurrentPrinter.copyFrom(printer); 1320 refreshCurrentPrinter(); 1321 return; 1322 } 1323 1324 // If the current printer became unavailable or its 1325 // capabilities go away, we update the UI and add a 1326 // timeout to declare the printer as unavailable. 1327 if ((mCurrentPrinter.getStatus() != PrinterInfo.STATUS_UNAVAILABLE 1328 && printer.getStatus() == PrinterInfo.STATUS_UNAVAILABLE) 1329 || (mCurrentPrinter.getCapabilities() != null 1330 && printer.getCapabilities() == null)) { 1331 if (!mCapabilitiesTimeout.isPosted()) { 1332 mCapabilitiesTimeout.post(); 1333 } 1334 mCurrentPrinter.copyFrom(printer); 1335 updateUi(); 1336 return; 1337 } 1338 1339 // We just refreshed the current printer. 1340 if (printer.getCapabilities() != null 1341 && mCapabilitiesTimeout.isPosted()) { 1342 mCapabilitiesTimeout.remove(); 1343 updatePrintAttributes(printer.getCapabilities()); 1344 updateUi(); 1345 mController.update(); 1346 } 1347 1348 // Update the UI if capabilities changed. 1349 boolean capabilitiesChanged = false; 1350 1351 if (mCurrentPrinter.getCapabilities() == null) { 1352 if (printer.getCapabilities() != null) { 1353 capabilitiesChanged = true; 1354 } 1355 } else if (!mCurrentPrinter.getCapabilities().equals( 1356 printer.getCapabilities())) { 1357 capabilitiesChanged = true; 1358 } 1359 1360 // Update the UI if the status changed. 1361 final boolean statusChanged = mCurrentPrinter.getStatus() 1362 != printer.getStatus(); 1363 1364 // Update the printer with the latest info. 1365 if (!mCurrentPrinter.equals(printer)) { 1366 mCurrentPrinter.copyFrom(printer); 1367 } 1368 1369 if (capabilitiesChanged || statusChanged) { 1370 // If something changed during update... 1371 if (updateUi() || !mController.hasPerformedLayout()) { 1372 // Update the document. 1373 mController.update(); 1374 } 1375 } 1376 1377 break; 1378 } 1379 } 1380 } 1381 } 1382 1383 @Override 1384 public void onInvalidated() { 1385 /* do nothing - we always have one fake PDF printer */ 1386 } 1387 }); 1388 1389 // Media size. 1390 mMediaSizeSpinnerAdapter = new ArrayAdapter<SpinnerItem<MediaSize>>( 1391 PrintJobConfigActivity.this, 1392 R.layout.spinner_dropdown_item, R.id.title); 1393 1394 // Color mode. 1395 mColorModeSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>( 1396 PrintJobConfigActivity.this, 1397 R.layout.spinner_dropdown_item, R.id.title); 1398 1399 // Orientation 1400 mOrientationSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>( 1401 PrintJobConfigActivity.this, 1402 R.layout.spinner_dropdown_item, R.id.title); 1403 String[] orientationLabels = getResources().getStringArray( 1404 R.array.orientation_labels); 1405 mOrientationSpinnerAdapter.add(new SpinnerItem<Integer>( 1406 ORIENTATION_PORTRAIT, orientationLabels[0])); 1407 mOrientationSpinnerAdapter.add(new SpinnerItem<Integer>( 1408 ORIENTATION_LANDSCAPE, orientationLabels[1])); 1409 1410 // Range options 1411 mRangeOptionsSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>( 1412 PrintJobConfigActivity.this, 1413 R.layout.spinner_dropdown_item, R.id.title); 1414 final int[] rangeOptionsValues = getResources().getIntArray( 1415 R.array.page_options_values); 1416 String[] rangeOptionsLabels = getResources().getStringArray( 1417 R.array.page_options_labels); 1418 final int rangeOptionsCount = rangeOptionsLabels.length; 1419 for (int i = 0; i < rangeOptionsCount; i++) { 1420 mRangeOptionsSpinnerAdapter.add(new SpinnerItem<Integer>( 1421 rangeOptionsValues[i], rangeOptionsLabels[i])); 1422 } 1423 1424 showUi(UI_EDITING_PRINT_JOB, null); 1425 bindUi(); 1426 updateUi(); 1427 } 1428 1429 public void reselectCurrentPrinter() { 1430 if (mCurrentPrinter != null) { 1431 // TODO: While the data did not change and we set the adapter 1432 // to a newly inflated spinner, the latter does not show the 1433 // current item unless we poke the adapter. This requires more 1434 // investigation. Maybe an optimization in AdapterView does not 1435 // call into the adapter if the view is not visible which is the 1436 // case when we set the adapter. 1437 mDestinationSpinnerAdapter.notifyDataSetChanged(); 1438 final int position = mDestinationSpinnerAdapter.getPrinterIndex( 1439 mCurrentPrinter.getId()); 1440 mDestinationSpinner.setSelection(position); 1441 } 1442 } 1443 1444 public void refreshCurrentPrinter() { 1445 PrinterInfo printer = (PrinterInfo) mDestinationSpinner.getSelectedItem(); 1446 if (printer != null) { 1447 FusedPrintersProvider printersLoader = (FusedPrintersProvider) 1448 (Loader<?>) getLoaderManager().getLoader( 1449 LOADER_ID_PRINTERS_LOADER); 1450 if (printersLoader != null) { 1451 printersLoader.setTrackedPrinter(printer.getId()); 1452 } 1453 } 1454 } 1455 1456 public void addCurrentPrinterToHistory() { 1457 PrinterInfo printer = (PrinterInfo) mDestinationSpinner.getSelectedItem(); 1458 PrinterId fakePdfPritnerId = mDestinationSpinnerAdapter.mFakePdfPrinter.getId(); 1459 if (printer != null && !printer.getId().equals(fakePdfPritnerId)) { 1460 FusedPrintersProvider printersLoader = (FusedPrintersProvider) 1461 (Loader<?>) getLoaderManager().getLoader( 1462 LOADER_ID_PRINTERS_LOADER); 1463 if (printersLoader != null) { 1464 printersLoader.addHistoricalPrinter(printer); 1465 } 1466 } 1467 } 1468 1469 public void updateFromAdvancedOptions(PrintJobInfo printJobInfo) { 1470 boolean updateContent = false; 1471 1472 // Copies. 1473 mCopiesEditText.setText(String.valueOf(printJobInfo.getCopies())); 1474 1475 // Media size and orientation 1476 PrintAttributes attributes = printJobInfo.getAttributes(); 1477 if (!mCurrPrintAttributes.getMediaSize().equals(attributes.getMediaSize())) { 1478 final int mediaSizeCount = mMediaSizeSpinnerAdapter.getCount(); 1479 for (int i = 0; i < mediaSizeCount; i++) { 1480 MediaSize mediaSize = mMediaSizeSpinnerAdapter.getItem(i).value; 1481 if (mediaSize.asPortrait().equals(attributes.getMediaSize().asPortrait())) { 1482 updateContent = true; 1483 mCurrPrintAttributes.setMediaSize(attributes.getMediaSize()); 1484 mMediaSizeSpinner.setSelection(i); 1485 mIgnoreNextMediaSizeChange = true; 1486 if (attributes.getMediaSize().isPortrait()) { 1487 mOrientationSpinner.setSelection(0); 1488 mIgnoreNextOrientationChange = true; 1489 } else { 1490 mOrientationSpinner.setSelection(1); 1491 mIgnoreNextOrientationChange = true; 1492 } 1493 break; 1494 } 1495 } 1496 } 1497 1498 // Color mode. 1499 final int colorMode = attributes.getColorMode(); 1500 if (mCurrPrintAttributes.getColorMode() != colorMode) { 1501 if (colorMode == PrintAttributes.COLOR_MODE_MONOCHROME) { 1502 updateContent = true; 1503 mColorModeSpinner.setSelection(0); 1504 mIgnoreNextColorChange = true; 1505 mCurrPrintAttributes.setColorMode(attributes.getColorMode()); 1506 } else if (colorMode == PrintAttributes.COLOR_MODE_COLOR) { 1507 updateContent = true; 1508 mColorModeSpinner.setSelection(1); 1509 mIgnoreNextColorChange = true; 1510 mCurrPrintAttributes.setColorMode(attributes.getColorMode()); 1511 } 1512 } 1513 1514 // Range. 1515 PageRange[] pageRanges = printJobInfo.getPages(); 1516 if (pageRanges != null && pageRanges.length > 0) { 1517 pageRanges = PageRangeUtils.normalize(pageRanges); 1518 final int pageRangeCount = pageRanges.length; 1519 if (pageRangeCount == 1 && pageRanges[0] == PageRange.ALL_PAGES) { 1520 mRangeOptionsSpinner.setSelection(0); 1521 } else { 1522 final int pageCount = mDocument.info.getPageCount(); 1523 if (pageRanges[0].getStart() >= 0 1524 && pageRanges[pageRanges.length - 1].getEnd() < pageCount) { 1525 mRangeOptionsSpinner.setSelection(1); 1526 StringBuilder builder = new StringBuilder(); 1527 for (int i = 0; i < pageRangeCount; i++) { 1528 if (builder.length() > 0) { 1529 builder.append(','); 1530 } 1531 PageRange pageRange = pageRanges[i]; 1532 final int shownStartPage = pageRange.getStart() + 1; 1533 final int shownEndPage = pageRange.getEnd() + 1; 1534 builder.append(shownStartPage); 1535 if (shownStartPage != shownEndPage) { 1536 builder.append('-'); 1537 builder.append(shownEndPage); 1538 } 1539 } 1540 mPageRangeEditText.setText(builder.toString()); 1541 } 1542 } 1543 } 1544 1545 // Update the advanced options. 1546 mSpoolerProvider.getSpooler().setPrintJobAdvancedOptionsNoPersistence( 1547 mPrintJobId, printJobInfo.getAdvancedOptions()); 1548 1549 // Update the content if needed. 1550 if (updateContent) { 1551 mController.update(); 1552 } 1553 } 1554 1555 public void ensurePrinterSelected(PrinterId printerId) { 1556 // If the printer is not present maybe the loader is not 1557 // updated yet. In this case make a note and as soon as 1558 // the printer appears will will select it. 1559 if (!selectPrinter(printerId)) { 1560 mNextPrinterId = printerId; 1561 } 1562 } 1563 1564 public boolean selectPrinter(PrinterId printerId) { 1565 mDestinationSpinnerAdapter.ensurePrinterInVisibleAdapterPosition(printerId); 1566 final int position = mDestinationSpinnerAdapter.getPrinterIndex(printerId); 1567 if (position != AdapterView.INVALID_POSITION 1568 && position != mDestinationSpinner.getSelectedItemPosition()) { 1569 Object item = mDestinationSpinnerAdapter.getItem(position); 1570 mCurrentPrinter = (PrinterInfo) item; 1571 mDestinationSpinner.setSelection(position); 1572 return true; 1573 } 1574 return false; 1575 } 1576 1577 public void ensureCurrentPrinterSelected() { 1578 if (mCurrentPrinter != null) { 1579 selectPrinter(mCurrentPrinter.getId()); 1580 } 1581 } 1582 1583 public boolean isPrintingToPdf() { 1584 return mDestinationSpinner.getSelectedItem() 1585 == mDestinationSpinnerAdapter.mFakePdfPrinter; 1586 } 1587 1588 public boolean shouldCloseOnTouch(MotionEvent event) { 1589 if (event.getAction() != MotionEvent.ACTION_DOWN) { 1590 return false; 1591 } 1592 1593 final int[] locationInWindow = new int[2]; 1594 mContentContainer.getLocationInWindow(locationInWindow); 1595 1596 final int windowTouchSlop = ViewConfiguration.get(PrintJobConfigActivity.this) 1597 .getScaledWindowTouchSlop(); 1598 final int eventX = (int) event.getX(); 1599 final int eventY = (int) event.getY(); 1600 final int lenientWindowLeft = locationInWindow[0] - windowTouchSlop; 1601 final int lenientWindowRight = lenientWindowLeft + mContentContainer.getWidth() 1602 + windowTouchSlop; 1603 final int lenientWindowTop = locationInWindow[1] - windowTouchSlop; 1604 final int lenientWindowBottom = lenientWindowTop + mContentContainer.getHeight() 1605 + windowTouchSlop; 1606 1607 if (eventX < lenientWindowLeft || eventX > lenientWindowRight 1608 || eventY < lenientWindowTop || eventY > lenientWindowBottom) { 1609 return true; 1610 } 1611 return false; 1612 } 1613 1614 public boolean isShwoingGeneratingPrintJobUi() { 1615 return (mCurrentUi == UI_GENERATING_PRINT_JOB); 1616 } 1617 1618 public void showUi(int ui, final Runnable postSwitchCallback) { 1619 if (ui == UI_NONE) { 1620 throw new IllegalStateException("cannot remove the ui"); 1621 } 1622 1623 if (mCurrentUi == ui) { 1624 return; 1625 } 1626 1627 final int oldUi = mCurrentUi; 1628 mCurrentUi = ui; 1629 1630 switch (oldUi) { 1631 case UI_NONE: { 1632 switch (ui) { 1633 case UI_EDITING_PRINT_JOB: { 1634 doUiSwitch(R.layout.print_job_config_activity_content_editing); 1635 registerPrintButtonClickListener(); 1636 if (postSwitchCallback != null) { 1637 postSwitchCallback.run(); 1638 } 1639 } break; 1640 1641 case UI_GENERATING_PRINT_JOB: { 1642 doUiSwitch(R.layout.print_job_config_activity_content_generating); 1643 registerCancelButtonClickListener(); 1644 if (postSwitchCallback != null) { 1645 postSwitchCallback.run(); 1646 } 1647 } break; 1648 } 1649 } break; 1650 1651 case UI_EDITING_PRINT_JOB: { 1652 switch (ui) { 1653 case UI_GENERATING_PRINT_JOB: { 1654 animateUiSwitch(R.layout.print_job_config_activity_content_generating, 1655 new Runnable() { 1656 @Override 1657 public void run() { 1658 registerCancelButtonClickListener(); 1659 if (postSwitchCallback != null) { 1660 postSwitchCallback.run(); 1661 } 1662 } 1663 }, 1664 new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 1665 ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); 1666 } break; 1667 1668 case UI_ERROR: { 1669 animateUiSwitch(R.layout.print_job_config_activity_content_error, 1670 new Runnable() { 1671 @Override 1672 public void run() { 1673 registerOkButtonClickListener(); 1674 if (postSwitchCallback != null) { 1675 postSwitchCallback.run(); 1676 } 1677 } 1678 }, 1679 new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 1680 ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); 1681 } break; 1682 } 1683 } break; 1684 1685 case UI_GENERATING_PRINT_JOB: { 1686 switch (ui) { 1687 case UI_EDITING_PRINT_JOB: { 1688 animateUiSwitch(R.layout.print_job_config_activity_content_editing, 1689 new Runnable() { 1690 @Override 1691 public void run() { 1692 registerPrintButtonClickListener(); 1693 if (postSwitchCallback != null) { 1694 postSwitchCallback.run(); 1695 } 1696 } 1697 }, 1698 new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1699 ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER)); 1700 } break; 1701 1702 case UI_ERROR: { 1703 animateUiSwitch(R.layout.print_job_config_activity_content_error, 1704 new Runnable() { 1705 @Override 1706 public void run() { 1707 registerOkButtonClickListener(); 1708 if (postSwitchCallback != null) { 1709 postSwitchCallback.run(); 1710 } 1711 } 1712 }, 1713 new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 1714 ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); 1715 } break; 1716 } 1717 } break; 1718 1719 case UI_ERROR: { 1720 switch (ui) { 1721 case UI_EDITING_PRINT_JOB: { 1722 animateUiSwitch(R.layout.print_job_config_activity_content_editing, 1723 new Runnable() { 1724 @Override 1725 public void run() { 1726 registerPrintButtonClickListener(); 1727 if (postSwitchCallback != null) { 1728 postSwitchCallback.run(); 1729 } 1730 } 1731 }, 1732 new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1733 ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER)); 1734 } break; 1735 } 1736 } break; 1737 } 1738 } 1739 1740 private void registerAdvancedPrintOptionsButtonClickListener() { 1741 Button advancedOptionsButton = (Button) findViewById(R.id.advanced_settings_button); 1742 advancedOptionsButton.setOnClickListener(new OnClickListener() { 1743 @Override 1744 public void onClick(View v) { 1745 ComponentName serviceName = mCurrentPrinter.getId().getServiceName(); 1746 String activityName = getAdvancedOptionsActivityName(serviceName); 1747 if (TextUtils.isEmpty(activityName)) { 1748 return; 1749 } 1750 Intent intent = new Intent(Intent.ACTION_MAIN); 1751 intent.setComponent(new ComponentName(serviceName.getPackageName(), 1752 activityName)); 1753 1754 List<ResolveInfo> resolvedActivities = getPackageManager() 1755 .queryIntentActivities(intent, 0); 1756 if (resolvedActivities.isEmpty()) { 1757 return; 1758 } 1759 // The activity is a component name, therefore it is one or none. 1760 if (resolvedActivities.get(0).activityInfo.exported) { 1761 PrintJobInfo printJobInfo = mSpoolerProvider.getSpooler().getPrintJobInfo( 1762 mPrintJobId, PrintManager.APP_ID_ANY); 1763 intent.putExtra(PrintService.EXTRA_PRINT_JOB_INFO, printJobInfo); 1764 // TODO: Make this an API for the next release. 1765 intent.putExtra("android.intent.extra.print.EXTRA_PRINTER_INFO", 1766 mCurrentPrinter); 1767 try { 1768 startActivityForResult(intent, 1769 ACTIVITY_POPULATE_ADVANCED_PRINT_OPTIONS); 1770 } catch (ActivityNotFoundException anfe) { 1771 Log.e(LOG_TAG, "Error starting activity for intent: " + intent, anfe); 1772 } 1773 } 1774 } 1775 }); 1776 } 1777 1778 private void registerPrintButtonClickListener() { 1779 Button printButton = (Button) findViewById(R.id.print_button); 1780 printButton.setOnClickListener(new OnClickListener() { 1781 @Override 1782 public void onClick(View v) { 1783 PrinterInfo printer = (PrinterInfo) mDestinationSpinner.getSelectedItem(); 1784 if (printer != null) { 1785 mEditor.confirmPrint(); 1786 mController.update(); 1787 if (!printer.equals(mDestinationSpinnerAdapter.mFakePdfPrinter)) { 1788 mEditor.refreshCurrentPrinter(); 1789 } 1790 } else { 1791 mEditor.cancel(); 1792 PrintJobConfigActivity.this.finish(); 1793 } 1794 } 1795 }); 1796 } 1797 1798 private void registerCancelButtonClickListener() { 1799 Button cancelButton = (Button) findViewById(R.id.cancel_button); 1800 cancelButton.setOnClickListener(new OnClickListener() { 1801 @Override 1802 public void onClick(View v) { 1803 if (!mController.isWorking()) { 1804 PrintJobConfigActivity.this.finish(); 1805 } 1806 mEditor.cancel(); 1807 } 1808 }); 1809 } 1810 1811 private void registerOkButtonClickListener() { 1812 Button okButton = (Button) findViewById(R.id.ok_button); 1813 okButton.setOnClickListener(new OnClickListener() { 1814 @Override 1815 public void onClick(View v) { 1816 mEditor.showUi(Editor.UI_EDITING_PRINT_JOB, new Runnable() { 1817 @Override 1818 public void run() { 1819 // Start over with a clean slate. 1820 mOldPrintAttributes.clear(); 1821 mController.initialize(); 1822 mEditor.initialize(); 1823 mEditor.bindUi(); 1824 mEditor.reselectCurrentPrinter(); 1825 if (!mController.hasPerformedLayout()) { 1826 mController.update(); 1827 } 1828 } 1829 }); 1830 } 1831 }); 1832 } 1833 1834 private void doUiSwitch(int showLayoutId) { 1835 ViewGroup contentContainer = (ViewGroup) findViewById(R.id.content_container); 1836 contentContainer.removeAllViews(); 1837 getLayoutInflater().inflate(showLayoutId, contentContainer, true); 1838 } 1839 1840 private void animateUiSwitch(int showLayoutId, final Runnable beforeShowNewUiAction, 1841 final LayoutParams containerParams) { 1842 // Find everything we will shuffle around. 1843 final ViewGroup contentContainer = (ViewGroup) findViewById(R.id.content_container); 1844 final View hidingView = contentContainer.getChildAt(0); 1845 final View showingView = getLayoutInflater().inflate(showLayoutId, 1846 null, false); 1847 1848 // First animation - fade out the old content. 1849 AutoCancellingAnimator.animate(hidingView).alpha(0.0f) 1850 .withLayer().withEndAction(new Runnable() { 1851 @Override 1852 public void run() { 1853 hidingView.setVisibility(View.INVISIBLE); 1854 1855 // Prepare the new content with correct size and alpha. 1856 showingView.setMinimumWidth(contentContainer.getWidth()); 1857 showingView.setAlpha(0.0f); 1858 1859 // Compute how to much shrink /stretch the content. 1860 final int widthSpec = MeasureSpec.makeMeasureSpec( 1861 contentContainer.getWidth(), MeasureSpec.UNSPECIFIED); 1862 final int heightSpec = MeasureSpec.makeMeasureSpec( 1863 contentContainer.getHeight(), MeasureSpec.UNSPECIFIED); 1864 showingView.measure(widthSpec, heightSpec); 1865 final float scaleY = (float) showingView.getMeasuredHeight() 1866 / (float) contentContainer.getHeight(); 1867 1868 // Second animation - resize the container. 1869 AutoCancellingAnimator.animate(contentContainer).scaleY(scaleY) 1870 .withEndAction(new Runnable() { 1871 @Override 1872 public void run() { 1873 // Swap the old and the new content. 1874 contentContainer.removeAllViews(); 1875 contentContainer.setScaleY(1.0f); 1876 contentContainer.addView(showingView); 1877 1878 contentContainer.setLayoutParams(containerParams); 1879 1880 beforeShowNewUiAction.run(); 1881 1882 // Third animation - show the new content. 1883 AutoCancellingAnimator.animate(showingView).alpha(1.0f); 1884 } 1885 }); 1886 } 1887 }); 1888 } 1889 1890 public void initialize() { 1891 mEditorState = EDITOR_STATE_INITIALIZED; 1892 } 1893 1894 public boolean isCancelled() { 1895 return mEditorState == EDITOR_STATE_CANCELLED; 1896 } 1897 1898 public void cancel() { 1899 mEditorState = EDITOR_STATE_CANCELLED; 1900 mController.cancel(); 1901 updateUi(); 1902 } 1903 1904 public boolean isDone() { 1905 return isPrintConfirmed() || isCancelled(); 1906 } 1907 1908 public boolean isPrintConfirmed() { 1909 return mEditorState == EDITOR_STATE_CONFIRMED_PRINT; 1910 } 1911 1912 public void confirmPrint() { 1913 addCurrentPrinterToHistory(); 1914 mEditorState = EDITOR_STATE_CONFIRMED_PRINT; 1915 showUi(UI_GENERATING_PRINT_JOB, null); 1916 } 1917 1918 public PageRange[] getRequestedPages() { 1919 if (hasErrors()) { 1920 return null; 1921 } 1922 if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) { 1923 List<PageRange> pageRanges = new ArrayList<PageRange>(); 1924 mStringCommaSplitter.setString(mPageRangeEditText.getText().toString()); 1925 1926 while (mStringCommaSplitter.hasNext()) { 1927 String range = mStringCommaSplitter.next().trim(); 1928 if (TextUtils.isEmpty(range)) { 1929 continue; 1930 } 1931 final int dashIndex = range.indexOf('-'); 1932 final int fromIndex; 1933 final int toIndex; 1934 1935 if (dashIndex > 0) { 1936 fromIndex = Integer.parseInt(range.substring(0, dashIndex).trim()) - 1; 1937 // It is possible that the dash is at the end since the input 1938 // verification can has to allow the user to keep entering if 1939 // this would lead to a valid input. So we handle this. 1940 toIndex = (dashIndex < range.length() - 1) 1941 ? Integer.parseInt(range.substring(dashIndex + 1, 1942 range.length()).trim()) - 1 : fromIndex; 1943 } else { 1944 fromIndex = toIndex = Integer.parseInt(range) - 1; 1945 } 1946 1947 PageRange pageRange = new PageRange(Math.min(fromIndex, toIndex), 1948 Math.max(fromIndex, toIndex)); 1949 pageRanges.add(pageRange); 1950 } 1951 1952 PageRange[] pageRangesArray = new PageRange[pageRanges.size()]; 1953 pageRanges.toArray(pageRangesArray); 1954 1955 return PageRangeUtils.normalize(pageRangesArray); 1956 } 1957 1958 return ALL_PAGES_ARRAY; 1959 } 1960 1961 private void bindUi() { 1962 if (mCurrentUi != UI_EDITING_PRINT_JOB) { 1963 return; 1964 } 1965 1966 // Content container 1967 mContentContainer = findViewById(R.id.content_container); 1968 1969 // Copies 1970 mCopiesEditText = (EditText) findViewById(R.id.copies_edittext); 1971 mCopiesEditText.setOnFocusChangeListener(mFocusListener); 1972 mCopiesEditText.setText(MIN_COPIES_STRING); 1973 mCopiesEditText.setSelection(mCopiesEditText.getText().length()); 1974 mCopiesEditText.addTextChangedListener(mCopiesTextWatcher); 1975 if (!TextUtils.equals(mCopiesEditText.getText(), MIN_COPIES_STRING)) { 1976 mIgnoreNextCopiesChange = true; 1977 } 1978 mSpoolerProvider.getSpooler().setPrintJobCopiesNoPersistence( 1979 mPrintJobId, MIN_COPIES); 1980 1981 // Destination. 1982 mDestinationSpinner = (Spinner) findViewById(R.id.destination_spinner); 1983 mDestinationSpinner.setDropDownWidth(ViewGroup.LayoutParams.MATCH_PARENT); 1984 mDestinationSpinner.setAdapter(mDestinationSpinnerAdapter); 1985 mDestinationSpinner.setOnItemSelectedListener(mOnItemSelectedListener); 1986 if (mDestinationSpinnerAdapter.getCount() > 0) { 1987 mIgnoreNextDestinationChange = true; 1988 } 1989 1990 // Media size. 1991 mMediaSizeSpinner = (Spinner) findViewById(R.id.paper_size_spinner); 1992 mMediaSizeSpinner.setAdapter(mMediaSizeSpinnerAdapter); 1993 mMediaSizeSpinner.setOnItemSelectedListener(mOnItemSelectedListener); 1994 if (mMediaSizeSpinnerAdapter.getCount() > 0) { 1995 mOldMediaSizeSelectionIndex = 0; 1996 } 1997 1998 // Color mode. 1999 mColorModeSpinner = (Spinner) findViewById(R.id.color_spinner); 2000 mColorModeSpinner.setAdapter(mColorModeSpinnerAdapter); 2001 mColorModeSpinner.setOnItemSelectedListener(mOnItemSelectedListener); 2002 if (mColorModeSpinnerAdapter.getCount() > 0) { 2003 mOldColorModeSelectionIndex = 0; 2004 } 2005 2006 // Orientation 2007 mOrientationSpinner = (Spinner) findViewById(R.id.orientation_spinner); 2008 mOrientationSpinner.setAdapter(mOrientationSpinnerAdapter); 2009 mOrientationSpinner.setOnItemSelectedListener(mOnItemSelectedListener); 2010 if (mOrientationSpinnerAdapter.getCount() > 0) { 2011 mIgnoreNextOrientationChange = true; 2012 } 2013 2014 // Range options 2015 mRangeOptionsTitle = (TextView) findViewById(R.id.range_options_title); 2016 mRangeOptionsSpinner = (Spinner) findViewById(R.id.range_options_spinner); 2017 mRangeOptionsSpinner.setAdapter(mRangeOptionsSpinnerAdapter); 2018 mRangeOptionsSpinner.setOnItemSelectedListener(mOnItemSelectedListener); 2019 if (mRangeOptionsSpinnerAdapter.getCount() > 0) { 2020 mIgnoreNextRangeOptionChange = true; 2021 } 2022 2023 // Page range 2024 mPageRangeTitle = (TextView) findViewById(R.id.page_range_title); 2025 mPageRangeEditText = (EditText) findViewById(R.id.page_range_edittext); 2026 mPageRangeEditText.setOnFocusChangeListener(mFocusListener); 2027 mPageRangeEditText.addTextChangedListener(mRangeTextWatcher); 2028 2029 // Advanced options button. 2030 mAdvancedPrintOptionsContainer = findViewById(R.id.advanced_settings_container); 2031 mAdvancedOptionsButton = (Button) findViewById(R.id.advanced_settings_button); 2032 registerAdvancedPrintOptionsButtonClickListener(); 2033 2034 // Print button 2035 mPrintButton = (Button) findViewById(R.id.print_button); 2036 registerPrintButtonClickListener(); 2037 } 2038 2039 public boolean updateUi() { 2040 if (mCurrentUi != UI_EDITING_PRINT_JOB) { 2041 return false; 2042 } 2043 if (isPrintConfirmed() || isCancelled()) { 2044 mDestinationSpinner.setEnabled(false); 2045 mCopiesEditText.setEnabled(false); 2046 mMediaSizeSpinner.setEnabled(false); 2047 mColorModeSpinner.setEnabled(false); 2048 mOrientationSpinner.setEnabled(false); 2049 mRangeOptionsSpinner.setEnabled(false); 2050 mPageRangeEditText.setEnabled(false); 2051 mPrintButton.setEnabled(false); 2052 mAdvancedOptionsButton.setEnabled(false); 2053 return false; 2054 } 2055 2056 // If a printer with capabilities is selected, then we enabled all options. 2057 boolean allOptionsEnabled = false; 2058 final int selectedIndex = mDestinationSpinner.getSelectedItemPosition(); 2059 if (selectedIndex >= 0) { 2060 Object item = mDestinationSpinnerAdapter.getItem(selectedIndex); 2061 if (item instanceof PrinterInfo) { 2062 PrinterInfo printer = (PrinterInfo) item; 2063 if (printer.getCapabilities() != null 2064 && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE) { 2065 allOptionsEnabled = true; 2066 } 2067 } 2068 } 2069 2070 if (!allOptionsEnabled) { 2071 mCopiesEditText.setEnabled(false); 2072 mMediaSizeSpinner.setEnabled(false); 2073 mColorModeSpinner.setEnabled(false); 2074 mOrientationSpinner.setEnabled(false); 2075 mRangeOptionsSpinner.setEnabled(false); 2076 mPageRangeEditText.setEnabled(false); 2077 mPrintButton.setEnabled(false); 2078 mAdvancedOptionsButton.setEnabled(false); 2079 return false; 2080 } else { 2081 boolean someAttributeSelectionChanged = false; 2082 2083 PrinterInfo printer = (PrinterInfo) mDestinationSpinner.getSelectedItem(); 2084 PrinterCapabilitiesInfo capabilities = printer.getCapabilities(); 2085 PrintAttributes defaultAttributes = printer.getCapabilities().getDefaults(); 2086 2087 // Media size. 2088 // Sort the media sizes based on the current locale. 2089 List<MediaSize> mediaSizes = new ArrayList<MediaSize>(capabilities.getMediaSizes()); 2090 Collections.sort(mediaSizes, mMediaSizeComparator); 2091 2092 // If the media sizes changed, we update the adapter and the spinner. 2093 boolean mediaSizesChanged = false; 2094 final int mediaSizeCount = mediaSizes.size(); 2095 if (mediaSizeCount != mMediaSizeSpinnerAdapter.getCount()) { 2096 mediaSizesChanged = true; 2097 } else { 2098 for (int i = 0; i < mediaSizeCount; i++) { 2099 if (!mediaSizes.get(i).equals(mMediaSizeSpinnerAdapter.getItem(i).value)) { 2100 mediaSizesChanged = true; 2101 break; 2102 } 2103 } 2104 } 2105 if (mediaSizesChanged) { 2106 // Remember the old media size to try selecting it again. 2107 int oldMediaSizeNewIndex = AdapterView.INVALID_POSITION; 2108 MediaSize oldMediaSize = mCurrPrintAttributes.getMediaSize(); 2109 2110 // Rebuild the adapter data. 2111 mMediaSizeSpinnerAdapter.clear(); 2112 for (int i = 0; i < mediaSizeCount; i++) { 2113 MediaSize mediaSize = mediaSizes.get(i); 2114 if (mediaSize.asPortrait().equals(oldMediaSize.asPortrait())) { 2115 // Update the index of the old selection. 2116 oldMediaSizeNewIndex = i; 2117 } 2118 mMediaSizeSpinnerAdapter.add(new SpinnerItem<MediaSize>( 2119 mediaSize, mediaSize.getLabel(getPackageManager()))); 2120 } 2121 2122 mMediaSizeSpinner.setEnabled(true); 2123 2124 if (oldMediaSizeNewIndex != AdapterView.INVALID_POSITION) { 2125 // Select the old media size - nothing really changed. 2126 setMediaSizeSpinnerSelectionNoCallback(oldMediaSizeNewIndex); 2127 } else { 2128 // Select the first or the default and mark if selection changed. 2129 final int mediaSizeIndex = Math.max(mediaSizes.indexOf( 2130 defaultAttributes.getMediaSize()), 0); 2131 setMediaSizeSpinnerSelectionNoCallback(mediaSizeIndex); 2132 if (oldMediaSize.isPortrait()) { 2133 mCurrPrintAttributes.setMediaSize(mMediaSizeSpinnerAdapter 2134 .getItem(mediaSizeIndex).value.asPortrait()); 2135 } else { 2136 mCurrPrintAttributes.setMediaSize(mMediaSizeSpinnerAdapter 2137 .getItem(mediaSizeIndex).value.asLandscape()); 2138 } 2139 someAttributeSelectionChanged = true; 2140 } 2141 } 2142 mMediaSizeSpinner.setEnabled(true); 2143 2144 // Color mode. 2145 final int colorModes = capabilities.getColorModes(); 2146 2147 // If the color modes changed, we update the adapter and the spinner. 2148 boolean colorModesChanged = false; 2149 if (Integer.bitCount(colorModes) != mColorModeSpinnerAdapter.getCount()) { 2150 colorModesChanged = true; 2151 } else { 2152 int remainingColorModes = colorModes; 2153 int adapterIndex = 0; 2154 while (remainingColorModes != 0) { 2155 final int colorBitOffset = Integer.numberOfTrailingZeros( 2156 remainingColorModes); 2157 final int colorMode = 1 << colorBitOffset; 2158 remainingColorModes &= ~colorMode; 2159 if (colorMode != mColorModeSpinnerAdapter.getItem(adapterIndex).value) { 2160 colorModesChanged = true; 2161 break; 2162 } 2163 adapterIndex++; 2164 } 2165 } 2166 if (colorModesChanged) { 2167 // Remember the old color mode to try selecting it again. 2168 int oldColorModeNewIndex = AdapterView.INVALID_POSITION; 2169 final int oldColorMode = mCurrPrintAttributes.getColorMode(); 2170 2171 // Rebuild the adapter data. 2172 mColorModeSpinnerAdapter.clear(); 2173 String[] colorModeLabels = getResources().getStringArray( 2174 R.array.color_mode_labels); 2175 int remainingColorModes = colorModes; 2176 while (remainingColorModes != 0) { 2177 final int colorBitOffset = Integer.numberOfTrailingZeros( 2178 remainingColorModes); 2179 final int colorMode = 1 << colorBitOffset; 2180 if (colorMode == oldColorMode) { 2181 // Update the index of the old selection. 2182 oldColorModeNewIndex = colorBitOffset; 2183 } 2184 remainingColorModes &= ~colorMode; 2185 mColorModeSpinnerAdapter.add(new SpinnerItem<Integer>(colorMode, 2186 colorModeLabels[colorBitOffset])); 2187 } 2188 mColorModeSpinner.setEnabled(true); 2189 if (oldColorModeNewIndex != AdapterView.INVALID_POSITION) { 2190 // Select the old color mode - nothing really changed. 2191 setColorModeSpinnerSelectionNoCallback(oldColorModeNewIndex); 2192 } else { 2193 final int selectedColorMode = colorModes & defaultAttributes.getColorMode(); 2194 final int itemCount = mColorModeSpinnerAdapter.getCount(); 2195 for (int i = 0; i < itemCount; i++) { 2196 SpinnerItem<Integer> item = mColorModeSpinnerAdapter.getItem(i); 2197 if (selectedColorMode == item.value) { 2198 setColorModeSpinnerSelectionNoCallback(i); 2199 mCurrPrintAttributes.setColorMode(selectedColorMode); 2200 someAttributeSelectionChanged = true; 2201 } 2202 } 2203 } 2204 } 2205 mColorModeSpinner.setEnabled(true); 2206 2207 // Orientation 2208 MediaSize mediaSize = mCurrPrintAttributes.getMediaSize(); 2209 if (mediaSize.isPortrait() 2210 && mOrientationSpinner.getSelectedItemPosition() != 0) { 2211 mIgnoreNextOrientationChange = true; 2212 mOrientationSpinner.setSelection(0); 2213 } else if (!mediaSize.isPortrait() 2214 && mOrientationSpinner.getSelectedItemPosition() != 1) { 2215 mIgnoreNextOrientationChange = true; 2216 mOrientationSpinner.setSelection(1); 2217 } 2218 mOrientationSpinner.setEnabled(true); 2219 2220 // Range options 2221 PrintDocumentInfo info = mDocument.info; 2222 if (info != null && info.getPageCount() > 0) { 2223 if (info.getPageCount() == 1) { 2224 mRangeOptionsSpinner.setEnabled(false); 2225 } else { 2226 mRangeOptionsSpinner.setEnabled(true); 2227 if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) { 2228 if (!mPageRangeEditText.isEnabled()) { 2229 mPageRangeEditText.setEnabled(true); 2230 mPageRangeEditText.setVisibility(View.VISIBLE); 2231 mPageRangeTitle.setVisibility(View.VISIBLE); 2232 mPageRangeEditText.requestFocus(); 2233 InputMethodManager imm = (InputMethodManager) 2234 getSystemService(INPUT_METHOD_SERVICE); 2235 imm.showSoftInput(mPageRangeEditText, 0); 2236 } 2237 } else { 2238 mPageRangeEditText.setEnabled(false); 2239 mPageRangeEditText.setVisibility(View.INVISIBLE); 2240 mPageRangeTitle.setVisibility(View.INVISIBLE); 2241 } 2242 } 2243 final int pageCount = mDocument.info.getPageCount(); 2244 String title = (pageCount != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) 2245 ? getString(R.string.label_pages, String.valueOf(pageCount)) 2246 : getString(R.string.page_count_unknown); 2247 mRangeOptionsTitle.setText(title); 2248 } else { 2249 if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) { 2250 mIgnoreNextRangeOptionChange = true; 2251 mRangeOptionsSpinner.setSelection(0); 2252 } 2253 mRangeOptionsSpinner.setEnabled(false); 2254 mRangeOptionsTitle.setText(getString(R.string.page_count_unknown)); 2255 mPageRangeEditText.setEnabled(false); 2256 mPageRangeEditText.setVisibility(View.INVISIBLE); 2257 mPageRangeTitle.setVisibility(View.INVISIBLE); 2258 } 2259 2260 // Advanced print options 2261 ComponentName serviceName = mCurrentPrinter.getId().getServiceName(); 2262 if (!TextUtils.isEmpty(getAdvancedOptionsActivityName(serviceName))) { 2263 mAdvancedPrintOptionsContainer.setVisibility(View.VISIBLE); 2264 mAdvancedOptionsButton.setEnabled(true); 2265 } else { 2266 mAdvancedPrintOptionsContainer.setVisibility(View.GONE); 2267 mAdvancedOptionsButton.setEnabled(false); 2268 } 2269 2270 // Print 2271 if (mDestinationSpinner.getSelectedItemId() 2272 != DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF) { 2273 String newText = getString(R.string.print_button); 2274 if (!TextUtils.equals(newText, mPrintButton.getText())) { 2275 mPrintButton.setText(R.string.print_button); 2276 } 2277 } else { 2278 String newText = getString(R.string.save_button); 2279 if (!TextUtils.equals(newText, mPrintButton.getText())) { 2280 mPrintButton.setText(R.string.save_button); 2281 } 2282 } 2283 if ((mRangeOptionsSpinner.getSelectedItemPosition() == 1 2284 && (TextUtils.isEmpty(mPageRangeEditText.getText()) || hasErrors())) 2285 || (mRangeOptionsSpinner.getSelectedItemPosition() == 0 2286 && (!mController.hasPerformedLayout() || hasErrors()))) { 2287 mPrintButton.setEnabled(false); 2288 } else { 2289 mPrintButton.setEnabled(true); 2290 } 2291 2292 // Copies 2293 if (mDestinationSpinner.getSelectedItemId() 2294 != DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF) { 2295 mCopiesEditText.setEnabled(true); 2296 } else { 2297 mCopiesEditText.setEnabled(false); 2298 } 2299 if (mCopiesEditText.getError() == null 2300 && TextUtils.isEmpty(mCopiesEditText.getText())) { 2301 mIgnoreNextCopiesChange = true; 2302 mCopiesEditText.setText(String.valueOf(MIN_COPIES)); 2303 mCopiesEditText.requestFocus(); 2304 } 2305 2306 return someAttributeSelectionChanged; 2307 } 2308 } 2309 2310 private String getAdvancedOptionsActivityName(ComponentName serviceName) { 2311 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); 2312 List<PrintServiceInfo> printServices = printManager.getEnabledPrintServices(); 2313 final int printServiceCount = printServices.size(); 2314 for (int i = 0; i < printServiceCount; i ++) { 2315 PrintServiceInfo printServiceInfo = printServices.get(i); 2316 ServiceInfo serviceInfo = printServiceInfo.getResolveInfo().serviceInfo; 2317 if (serviceInfo.name.equals(serviceName.getClassName()) 2318 && serviceInfo.packageName.equals(serviceName.getPackageName())) { 2319 return printServiceInfo.getAdvancedOptionsActivityName(); 2320 } 2321 } 2322 return null; 2323 } 2324 2325 private void setMediaSizeSpinnerSelectionNoCallback(int position) { 2326 if (mMediaSizeSpinner.getSelectedItemPosition() != position) { 2327 mOldMediaSizeSelectionIndex = position; 2328 mMediaSizeSpinner.setSelection(position); 2329 } 2330 } 2331 2332 private void setColorModeSpinnerSelectionNoCallback(int position) { 2333 if (mColorModeSpinner.getSelectedItemPosition() != position) { 2334 mOldColorModeSelectionIndex = position; 2335 mColorModeSpinner.setSelection(position); 2336 } 2337 } 2338 2339 private void startSelectPrinterActivity() { 2340 Intent intent = new Intent(PrintJobConfigActivity.this, 2341 SelectPrinterActivity.class); 2342 startActivityForResult(intent, ACTIVITY_REQUEST_SELECT_PRINTER); 2343 } 2344 2345 private boolean hasErrors() { 2346 if (mCopiesEditText.getError() != null) { 2347 return true; 2348 } 2349 return mPageRangeEditText.getVisibility() == View.VISIBLE 2350 && mPageRangeEditText.getError() != null; 2351 } 2352 2353 private final class SpinnerItem<T> { 2354 final T value; 2355 CharSequence label; 2356 2357 public SpinnerItem(T value, CharSequence label) { 2358 this.value = value; 2359 this.label = label; 2360 } 2361 2362 public String toString() { 2363 return label.toString(); 2364 } 2365 } 2366 2367 private final class WaitForPrinterCapabilitiesTimeout implements Runnable { 2368 private static final long GET_CAPABILITIES_TIMEOUT_MILLIS = 10000; // 10sec 2369 2370 private boolean mIsPosted; 2371 2372 public void post() { 2373 if (!mIsPosted) { 2374 mDestinationSpinner.postDelayed(this, 2375 GET_CAPABILITIES_TIMEOUT_MILLIS); 2376 mIsPosted = true; 2377 } 2378 } 2379 2380 public void remove() { 2381 if (mIsPosted) { 2382 mIsPosted = false; 2383 mDestinationSpinner.removeCallbacks(this); 2384 } 2385 } 2386 2387 public boolean isPosted() { 2388 return mIsPosted; 2389 } 2390 2391 @Override 2392 public void run() { 2393 mIsPosted = false; 2394 if (mDestinationSpinner.getSelectedItemPosition() >= 0) { 2395 View itemView = mDestinationSpinner.getSelectedView(); 2396 TextView titleView = (TextView) itemView.findViewById(R.id.subtitle); 2397 try { 2398 PackageInfo packageInfo = getPackageManager().getPackageInfo( 2399 mCurrentPrinter.getId().getServiceName().getPackageName(), 0); 2400 CharSequence service = packageInfo.applicationInfo.loadLabel( 2401 getPackageManager()); 2402 String subtitle = getString(R.string.printer_unavailable, service.toString()); 2403 titleView.setText(subtitle); 2404 } catch (NameNotFoundException nnfe) { 2405 /* ignore */ 2406 } 2407 } 2408 } 2409 } 2410 2411 private final class DestinationAdapter extends BaseAdapter 2412 implements LoaderManager.LoaderCallbacks<List<PrinterInfo>>{ 2413 private final List<PrinterInfo> mPrinters = new ArrayList<PrinterInfo>(); 2414 2415 private PrinterInfo mFakePdfPrinter; 2416 2417 public DestinationAdapter() { 2418 getLoaderManager().initLoader(LOADER_ID_PRINTERS_LOADER, null, this); 2419 } 2420 2421 public int getPrinterIndex(PrinterId printerId) { 2422 for (int i = 0; i < getCount(); i++) { 2423 PrinterInfo printer = (PrinterInfo) getItem(i); 2424 if (printer != null && printer.getId().equals(printerId)) { 2425 return i; 2426 } 2427 } 2428 return AdapterView.INVALID_POSITION; 2429 } 2430 2431 public void ensurePrinterInVisibleAdapterPosition(PrinterId printerId) { 2432 final int printerCount = mPrinters.size(); 2433 for (int i = 0; i < printerCount; i++) { 2434 PrinterInfo printer = (PrinterInfo) mPrinters.get(i); 2435 if (printer.getId().equals(printerId)) { 2436 // If already in the list - do nothing. 2437 if (i < getCount() - 2) { 2438 return; 2439 } 2440 // Else replace the last one (two items are not printers). 2441 final int lastPrinterIndex = getCount() - 3; 2442 mPrinters.set(i, mPrinters.get(lastPrinterIndex)); 2443 mPrinters.set(lastPrinterIndex, printer); 2444 notifyDataSetChanged(); 2445 return; 2446 } 2447 } 2448 } 2449 2450 @Override 2451 public int getCount() { 2452 if (mFakePdfPrinter == null) { 2453 return 0; 2454 } 2455 return Math.min(mPrinters.size() + 2, DEST_ADAPTER_MAX_ITEM_COUNT); 2456 } 2457 2458 @Override 2459 public boolean isEnabled(int position) { 2460 Object item = getItem(position); 2461 if (item instanceof PrinterInfo) { 2462 PrinterInfo printer = (PrinterInfo) item; 2463 return printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE; 2464 } 2465 return true; 2466 } 2467 2468 @Override 2469 public Object getItem(int position) { 2470 if (mPrinters.isEmpty()) { 2471 if (position == 0 && mFakePdfPrinter != null) { 2472 return mFakePdfPrinter; 2473 } 2474 } else { 2475 if (position < 1) { 2476 return mPrinters.get(position); 2477 } 2478 if (position == 1 && mFakePdfPrinter != null) { 2479 return mFakePdfPrinter; 2480 } 2481 if (position < getCount() - 1) { 2482 return mPrinters.get(position - 1); 2483 } 2484 } 2485 return null; 2486 } 2487 2488 @Override 2489 public long getItemId(int position) { 2490 if (mPrinters.isEmpty()) { 2491 if (mFakePdfPrinter != null) { 2492 if (position == 0) { 2493 return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF; 2494 } else if (position == 1) { 2495 return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS; 2496 } 2497 } 2498 } else { 2499 if (position == 1 && mFakePdfPrinter != null) { 2500 return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF; 2501 } 2502 if (position == getCount() - 1) { 2503 return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS; 2504 } 2505 } 2506 return position; 2507 } 2508 2509 @Override 2510 public View getDropDownView(int position, View convertView, 2511 ViewGroup parent) { 2512 View view = getView(position, convertView, parent); 2513 view.setEnabled(isEnabled(position)); 2514 return view; 2515 } 2516 2517 @Override 2518 public View getView(int position, View convertView, ViewGroup parent) { 2519 if (convertView == null) { 2520 convertView = getLayoutInflater().inflate( 2521 R.layout.printer_dropdown_item, parent, false); 2522 } 2523 2524 CharSequence title = null; 2525 CharSequence subtitle = null; 2526 Drawable icon = null; 2527 2528 if (mPrinters.isEmpty()) { 2529 if (position == 0 && mFakePdfPrinter != null) { 2530 PrinterInfo printer = (PrinterInfo) getItem(position); 2531 title = printer.getName(); 2532 } else if (position == 1) { 2533 title = getString(R.string.all_printers); 2534 } 2535 } else { 2536 if (position == 1 && mFakePdfPrinter != null) { 2537 PrinterInfo printer = (PrinterInfo) getItem(position); 2538 title = printer.getName(); 2539 } else if (position == getCount() - 1) { 2540 title = getString(R.string.all_printers); 2541 } else { 2542 PrinterInfo printer = (PrinterInfo) getItem(position); 2543 title = printer.getName(); 2544 try { 2545 PackageInfo packageInfo = getPackageManager().getPackageInfo( 2546 printer.getId().getServiceName().getPackageName(), 0); 2547 subtitle = packageInfo.applicationInfo.loadLabel(getPackageManager()); 2548 icon = packageInfo.applicationInfo.loadIcon(getPackageManager()); 2549 } catch (NameNotFoundException nnfe) { 2550 /* ignore */ 2551 } 2552 } 2553 } 2554 2555 TextView titleView = (TextView) convertView.findViewById(R.id.title); 2556 titleView.setText(title); 2557 2558 TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle); 2559 if (!TextUtils.isEmpty(subtitle)) { 2560 subtitleView.setText(subtitle); 2561 subtitleView.setVisibility(View.VISIBLE); 2562 } else { 2563 subtitleView.setText(null); 2564 subtitleView.setVisibility(View.GONE); 2565 } 2566 2567 ImageView iconView = (ImageView) convertView.findViewById(R.id.icon); 2568 if (icon != null) { 2569 iconView.setImageDrawable(icon); 2570 iconView.setVisibility(View.VISIBLE); 2571 } else { 2572 iconView.setVisibility(View.INVISIBLE); 2573 } 2574 2575 return convertView; 2576 } 2577 2578 @Override 2579 public Loader<List<PrinterInfo>> onCreateLoader(int id, Bundle args) { 2580 if (id == LOADER_ID_PRINTERS_LOADER) { 2581 return new FusedPrintersProvider(PrintJobConfigActivity.this); 2582 } 2583 return null; 2584 } 2585 2586 @Override 2587 public void onLoadFinished(Loader<List<PrinterInfo>> loader, 2588 List<PrinterInfo> printers) { 2589 // If this is the first load, create the fake PDF printer. 2590 // We do this to avoid flicker where the PDF printer is the 2591 // only one and as soon as the loader loads the favorites 2592 // it gets switched. Not a great user experience. 2593 if (mFakePdfPrinter == null) { 2594 mCurrentPrinter = mFakePdfPrinter = createFakePdfPrinter(); 2595 updatePrintAttributes(mCurrentPrinter.getCapabilities()); 2596 updateUi(); 2597 } 2598 2599 // We rearrange the printers if the user selects a printer 2600 // not shown in the initial short list. Therefore, we have 2601 // to keep the printer order. 2602 2603 // No old printers - do not bother keeping their position. 2604 if (mPrinters.isEmpty()) { 2605 mPrinters.addAll(printers); 2606 mEditor.ensureCurrentPrinterSelected(); 2607 notifyDataSetChanged(); 2608 return; 2609 } 2610 2611 // Add the new printers to a map. 2612 ArrayMap<PrinterId, PrinterInfo> newPrintersMap = 2613 new ArrayMap<PrinterId, PrinterInfo>(); 2614 final int printerCount = printers.size(); 2615 for (int i = 0; i < printerCount; i++) { 2616 PrinterInfo printer = printers.get(i); 2617 newPrintersMap.put(printer.getId(), printer); 2618 } 2619 2620 List<PrinterInfo> newPrinters = new ArrayList<PrinterInfo>(); 2621 2622 // Update printers we already have. 2623 final int oldPrinterCount = mPrinters.size(); 2624 for (int i = 0; i < oldPrinterCount; i++) { 2625 PrinterId oldPrinterId = mPrinters.get(i).getId(); 2626 PrinterInfo updatedPrinter = newPrintersMap.remove(oldPrinterId); 2627 if (updatedPrinter != null) { 2628 newPrinters.add(updatedPrinter); 2629 } 2630 } 2631 2632 // Add the rest of the new printers, i.e. what is left. 2633 newPrinters.addAll(newPrintersMap.values()); 2634 2635 mPrinters.clear(); 2636 mPrinters.addAll(newPrinters); 2637 2638 mEditor.ensureCurrentPrinterSelected(); 2639 notifyDataSetChanged(); 2640 } 2641 2642 @Override 2643 public void onLoaderReset(Loader<List<PrinterInfo>> loader) { 2644 mPrinters.clear(); 2645 notifyDataSetInvalidated(); 2646 } 2647 2648 2649 private PrinterInfo createFakePdfPrinter() { 2650 MediaSize defaultMediaSize = MediaSizeUtils.getDefault(PrintJobConfigActivity.this); 2651 2652 PrinterId printerId = new PrinterId(getComponentName(), "PDF printer"); 2653 2654 PrinterCapabilitiesInfo.Builder builder = 2655 new PrinterCapabilitiesInfo.Builder(printerId); 2656 2657 String[] mediaSizeIds = getResources().getStringArray( 2658 R.array.pdf_printer_media_sizes); 2659 final int mediaSizeIdCount = mediaSizeIds.length; 2660 for (int i = 0; i < mediaSizeIdCount; i++) { 2661 String id = mediaSizeIds[i]; 2662 MediaSize mediaSize = MediaSize.getStandardMediaSizeById(id); 2663 builder.addMediaSize(mediaSize, mediaSize.equals(defaultMediaSize)); 2664 } 2665 2666 builder.addResolution(new Resolution("PDF resolution", "PDF resolution", 2667 300, 300), true); 2668 builder.setColorModes(PrintAttributes.COLOR_MODE_COLOR 2669 | PrintAttributes.COLOR_MODE_MONOCHROME, 2670 PrintAttributes.COLOR_MODE_COLOR); 2671 2672 return new PrinterInfo.Builder(printerId, getString(R.string.save_as_pdf), 2673 PrinterInfo.STATUS_IDLE) 2674 .setCapabilities(builder.build()) 2675 .build(); 2676 } 2677 } 2678 } 2679 2680 /** 2681 * An instance of this class class is intended to be the first focusable 2682 * in a layout to which the system automatically gives focus. It performs 2683 * some voodoo to avoid the first tap on it to start an edit mode, rather 2684 * to bring up the IME, i.e. to get the behavior as if the view was not 2685 * focused. 2686 */ 2687 public static final class CustomEditText extends EditText { 2688 private boolean mClickedBeforeFocus; 2689 private CharSequence mError; 2690 2691 public CustomEditText(Context context, AttributeSet attrs) { 2692 super(context, attrs); 2693 } 2694 2695 @Override 2696 public boolean performClick() { 2697 super.performClick(); 2698 if (isFocused() && !mClickedBeforeFocus) { 2699 clearFocus(); 2700 requestFocus(); 2701 } 2702 mClickedBeforeFocus = true; 2703 return true; 2704 } 2705 2706 @Override 2707 public CharSequence getError() { 2708 return mError; 2709 } 2710 2711 @Override 2712 public void setError(CharSequence error, Drawable icon) { 2713 setCompoundDrawables(null, null, icon, null); 2714 mError = error; 2715 } 2716 2717 protected void onFocusChanged(boolean gainFocus, int direction, 2718 Rect previouslyFocusedRect) { 2719 if (!gainFocus) { 2720 mClickedBeforeFocus = false; 2721 } 2722 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 2723 } 2724 } 2725 2726 private static final class Document { 2727 public PrintDocumentInfo info; 2728 public PageRange[] pages; 2729 } 2730 2731 private static final class PageRangeUtils { 2732 2733 private static final Comparator<PageRange> sComparator = new Comparator<PageRange>() { 2734 @Override 2735 public int compare(PageRange lhs, PageRange rhs) { 2736 return lhs.getStart() - rhs.getStart(); 2737 } 2738 }; 2739 2740 private PageRangeUtils() { 2741 throw new UnsupportedOperationException(); 2742 } 2743 2744 public static boolean contains(PageRange[] ourRanges, PageRange[] otherRanges) { 2745 if (ourRanges == null || otherRanges == null) { 2746 return false; 2747 } 2748 2749 if (ourRanges.length == 1 2750 && PageRange.ALL_PAGES.equals(ourRanges[0])) { 2751 return true; 2752 } 2753 2754 ourRanges = normalize(ourRanges); 2755 otherRanges = normalize(otherRanges); 2756 2757 // Note that the code below relies on the ranges being normalized 2758 // which is they contain monotonically increasing non-intersecting 2759 // subranges whose start is less that or equal to the end. 2760 int otherRangeIdx = 0; 2761 final int ourRangeCount = ourRanges.length; 2762 final int otherRangeCount = otherRanges.length; 2763 for (int ourRangeIdx = 0; ourRangeIdx < ourRangeCount; ourRangeIdx++) { 2764 PageRange ourRange = ourRanges[ourRangeIdx]; 2765 for (; otherRangeIdx < otherRangeCount; otherRangeIdx++) { 2766 PageRange otherRange = otherRanges[otherRangeIdx]; 2767 if (otherRange.getStart() > ourRange.getEnd()) { 2768 break; 2769 } 2770 if (otherRange.getStart() < ourRange.getStart() 2771 || otherRange.getEnd() > ourRange.getEnd()) { 2772 return false; 2773 } 2774 } 2775 } 2776 if (otherRangeIdx < otherRangeCount) { 2777 return false; 2778 } 2779 return true; 2780 } 2781 2782 public static PageRange[] normalize(PageRange[] pageRanges) { 2783 if (pageRanges == null) { 2784 return null; 2785 } 2786 final int oldRangeCount = pageRanges.length; 2787 if (oldRangeCount <= 1) { 2788 return pageRanges; 2789 } 2790 Arrays.sort(pageRanges, sComparator); 2791 int newRangeCount = 1; 2792 for (int i = 0; i < oldRangeCount - 1; i++) { 2793 newRangeCount++; 2794 PageRange currentRange = pageRanges[i]; 2795 PageRange nextRange = pageRanges[i + 1]; 2796 if (currentRange.getEnd() + 1 >= nextRange.getStart()) { 2797 newRangeCount--; 2798 pageRanges[i] = null; 2799 pageRanges[i + 1] = new PageRange(currentRange.getStart(), 2800 Math.max(currentRange.getEnd(), nextRange.getEnd())); 2801 } 2802 } 2803 if (newRangeCount == oldRangeCount) { 2804 return pageRanges; 2805 } 2806 return Arrays.copyOfRange(pageRanges, oldRangeCount - newRangeCount, 2807 oldRangeCount); 2808 } 2809 2810 public static void offset(PageRange[] pageRanges, int offset) { 2811 if (offset == 0) { 2812 return; 2813 } 2814 final int pageRangeCount = pageRanges.length; 2815 for (int i = 0; i < pageRangeCount; i++) { 2816 final int start = pageRanges[i].getStart() + offset; 2817 final int end = pageRanges[i].getEnd() + offset; 2818 pageRanges[i] = new PageRange(start, end); 2819 } 2820 } 2821 } 2822 2823 private static final class AutoCancellingAnimator 2824 implements OnAttachStateChangeListener, Runnable { 2825 2826 private ViewPropertyAnimator mAnimator; 2827 2828 private boolean mCancelled; 2829 private Runnable mEndCallback; 2830 2831 public static AutoCancellingAnimator animate(View view) { 2832 ViewPropertyAnimator animator = view.animate(); 2833 AutoCancellingAnimator cancellingWrapper = 2834 new AutoCancellingAnimator(animator); 2835 view.addOnAttachStateChangeListener(cancellingWrapper); 2836 return cancellingWrapper; 2837 } 2838 2839 private AutoCancellingAnimator(ViewPropertyAnimator animator) { 2840 mAnimator = animator; 2841 } 2842 2843 public AutoCancellingAnimator alpha(float alpha) { 2844 mAnimator = mAnimator.alpha(alpha); 2845 return this; 2846 } 2847 2848 public void cancel() { 2849 mAnimator.cancel(); 2850 } 2851 2852 public AutoCancellingAnimator withLayer() { 2853 mAnimator = mAnimator.withLayer(); 2854 return this; 2855 } 2856 2857 public AutoCancellingAnimator withEndAction(Runnable callback) { 2858 mEndCallback = callback; 2859 mAnimator = mAnimator.withEndAction(this); 2860 return this; 2861 } 2862 2863 public AutoCancellingAnimator scaleY(float scale) { 2864 mAnimator = mAnimator.scaleY(scale); 2865 return this; 2866 } 2867 2868 @Override 2869 public void onViewAttachedToWindow(View v) { 2870 /* do nothing */ 2871 } 2872 2873 @Override 2874 public void onViewDetachedFromWindow(View v) { 2875 cancel(); 2876 } 2877 2878 @Override 2879 public void run() { 2880 if (!mCancelled) { 2881 mEndCallback.run(); 2882 } 2883 } 2884 } 2885 2886 private static final class PrintSpoolerProvider implements ServiceConnection { 2887 private final Context mContext; 2888 private final Runnable mCallback; 2889 2890 private PrintSpoolerService mSpooler; 2891 2892 public PrintSpoolerProvider(Context context, Runnable callback) { 2893 mContext = context; 2894 mCallback = callback; 2895 Intent intent = new Intent(mContext, PrintSpoolerService.class); 2896 mContext.bindService(intent, this, 0); 2897 } 2898 2899 public PrintSpoolerService getSpooler() { 2900 return mSpooler; 2901 } 2902 2903 public void destroy() { 2904 if (mSpooler != null) { 2905 mContext.unbindService(this); 2906 } 2907 } 2908 2909 @Override 2910 public void onServiceConnected(ComponentName name, IBinder service) { 2911 mSpooler = ((PrintSpoolerService.PrintSpooler) service).getService(); 2912 if (mSpooler != null) { 2913 mCallback.run(); 2914 } 2915 } 2916 2917 @Override 2918 public void onServiceDisconnected(ComponentName name) { 2919 /* do noting - we are in the same process */ 2920 } 2921 } 2922 2923 private static final class PrintDocumentAdapterObserver 2924 extends IPrintDocumentAdapterObserver.Stub { 2925 private final WeakReference<PrintJobConfigActivity> mWeakActvity; 2926 2927 public PrintDocumentAdapterObserver(PrintJobConfigActivity activity) { 2928 mWeakActvity = new WeakReference<PrintJobConfigActivity>(activity); 2929 } 2930 2931 @Override 2932 public void onDestroy() { 2933 final PrintJobConfigActivity activity = mWeakActvity.get(); 2934 if (activity != null) { 2935 activity.mController.mHandler.post(new Runnable() { 2936 @Override 2937 public void run() { 2938 if (activity.mController != null) { 2939 activity.mController.cancel(); 2940 } 2941 if (activity.mEditor != null) { 2942 activity.mEditor.cancel(); 2943 } 2944 activity.finish(); 2945 } 2946 }); 2947 } 2948 } 2949 } 2950 } 2951