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
17package com.android.printspooler;
18
19import android.app.Activity;
20import android.app.Dialog;
21import android.app.LoaderManager;
22import android.content.ActivityNotFoundException;
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.Loader;
27import android.content.ServiceConnection;
28import android.content.pm.PackageInfo;
29import android.content.pm.PackageManager.NameNotFoundException;
30import android.content.pm.ResolveInfo;
31import android.content.pm.ServiceInfo;
32import android.database.DataSetObserver;
33import android.graphics.Rect;
34import android.graphics.drawable.Drawable;
35import android.net.Uri;
36import android.os.AsyncTask;
37import android.os.Bundle;
38import android.os.Handler;
39import android.os.IBinder;
40import android.os.IBinder.DeathRecipient;
41import android.os.Looper;
42import android.os.Message;
43import android.os.RemoteException;
44import android.print.ILayoutResultCallback;
45import android.print.IPrintDocumentAdapter;
46import android.print.IPrintDocumentAdapterObserver;
47import android.print.IWriteResultCallback;
48import android.print.PageRange;
49import android.print.PrintAttributes;
50import android.print.PrintAttributes.Margins;
51import android.print.PrintAttributes.MediaSize;
52import android.print.PrintAttributes.Resolution;
53import android.print.PrintDocumentAdapter;
54import android.print.PrintDocumentInfo;
55import android.print.PrintJobId;
56import android.print.PrintJobInfo;
57import android.print.PrintManager;
58import android.print.PrinterCapabilitiesInfo;
59import android.print.PrinterId;
60import android.print.PrinterInfo;
61import android.printservice.PrintService;
62import android.printservice.PrintServiceInfo;
63import android.provider.DocumentsContract;
64import android.text.Editable;
65import android.text.TextUtils;
66import android.text.TextUtils.SimpleStringSplitter;
67import android.text.TextWatcher;
68import android.util.ArrayMap;
69import android.util.AttributeSet;
70import android.util.Log;
71import android.view.Gravity;
72import android.view.KeyEvent;
73import android.view.MotionEvent;
74import android.view.View;
75import android.view.View.MeasureSpec;
76import android.view.View.OnAttachStateChangeListener;
77import android.view.View.OnClickListener;
78import android.view.View.OnFocusChangeListener;
79import android.view.ViewConfiguration;
80import android.view.ViewGroup;
81import android.view.ViewGroup.LayoutParams;
82import android.view.ViewPropertyAnimator;
83import android.view.inputmethod.InputMethodManager;
84import android.widget.AdapterView;
85import android.widget.AdapterView.OnItemSelectedListener;
86import android.widget.ArrayAdapter;
87import android.widget.BaseAdapter;
88import android.widget.Button;
89import android.widget.EditText;
90import android.widget.FrameLayout;
91import android.widget.ImageView;
92import android.widget.Spinner;
93import android.widget.TextView;
94
95import com.android.printspooler.MediaSizeUtils.MediaSizeComparator;
96
97import libcore.io.IoUtils;
98
99import java.io.File;
100import java.io.FileInputStream;
101import java.io.FileNotFoundException;
102import java.io.IOException;
103import java.io.InputStream;
104import java.io.OutputStream;
105import java.lang.ref.WeakReference;
106import java.util.ArrayList;
107import java.util.Arrays;
108import java.util.Collections;
109import java.util.Comparator;
110import java.util.List;
111import java.util.concurrent.atomic.AtomicInteger;
112import java.util.regex.Matcher;
113import java.util.regex.Pattern;
114
115/**
116 * Activity for configuring a print job.
117 */
118public 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 selectedColorModeIndex = Integer.numberOfTrailingZeros(
2194                                    (colorModes & defaultAttributes.getColorMode()));
2195                        setColorModeSpinnerSelectionNoCallback(selectedColorModeIndex);
2196                        mCurrPrintAttributes.setColorMode(mColorModeSpinnerAdapter
2197                                .getItem(selectedColorModeIndex).value);
2198                        someAttributeSelectionChanged = true;
2199                    }
2200                }
2201                mColorModeSpinner.setEnabled(true);
2202
2203                // Orientation
2204                MediaSize mediaSize = mCurrPrintAttributes.getMediaSize();
2205                if (mediaSize.isPortrait()
2206                        && mOrientationSpinner.getSelectedItemPosition() != 0) {
2207                    mIgnoreNextOrientationChange = true;
2208                    mOrientationSpinner.setSelection(0);
2209                } else if (!mediaSize.isPortrait()
2210                        && mOrientationSpinner.getSelectedItemPosition() != 1) {
2211                    mIgnoreNextOrientationChange = true;
2212                    mOrientationSpinner.setSelection(1);
2213                }
2214                mOrientationSpinner.setEnabled(true);
2215
2216                // Range options
2217                PrintDocumentInfo info = mDocument.info;
2218                if (info != null && info.getPageCount() > 0) {
2219                    if (info.getPageCount() == 1) {
2220                        mRangeOptionsSpinner.setEnabled(false);
2221                    } else {
2222                        mRangeOptionsSpinner.setEnabled(true);
2223                        if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
2224                            if (!mPageRangeEditText.isEnabled()) {
2225                                mPageRangeEditText.setEnabled(true);
2226                                mPageRangeEditText.setVisibility(View.VISIBLE);
2227                                mPageRangeTitle.setVisibility(View.VISIBLE);
2228                                mPageRangeEditText.requestFocus();
2229                                InputMethodManager imm = (InputMethodManager)
2230                                        getSystemService(INPUT_METHOD_SERVICE);
2231                                imm.showSoftInput(mPageRangeEditText, 0);
2232                            }
2233                        } else {
2234                            mPageRangeEditText.setEnabled(false);
2235                            mPageRangeEditText.setVisibility(View.INVISIBLE);
2236                            mPageRangeTitle.setVisibility(View.INVISIBLE);
2237                        }
2238                    }
2239                    final int pageCount = mDocument.info.getPageCount();
2240                    String title = (pageCount != PrintDocumentInfo.PAGE_COUNT_UNKNOWN)
2241                            ? getString(R.string.label_pages, String.valueOf(pageCount))
2242                            : getString(R.string.page_count_unknown);
2243                    mRangeOptionsTitle.setText(title);
2244                } else {
2245                    if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
2246                        mIgnoreNextRangeOptionChange = true;
2247                        mRangeOptionsSpinner.setSelection(0);
2248                    }
2249                    mRangeOptionsSpinner.setEnabled(false);
2250                    mRangeOptionsTitle.setText(getString(R.string.page_count_unknown));
2251                    mPageRangeEditText.setEnabled(false);
2252                    mPageRangeEditText.setVisibility(View.INVISIBLE);
2253                    mPageRangeTitle.setVisibility(View.INVISIBLE);
2254                }
2255
2256                // Advanced print options
2257                ComponentName serviceName = mCurrentPrinter.getId().getServiceName();
2258                if (!TextUtils.isEmpty(getAdvancedOptionsActivityName(serviceName))) {
2259                    mAdvancedPrintOptionsContainer.setVisibility(View.VISIBLE);
2260                    mAdvancedOptionsButton.setEnabled(true);
2261                } else {
2262                    mAdvancedPrintOptionsContainer.setVisibility(View.GONE);
2263                    mAdvancedOptionsButton.setEnabled(false);
2264                }
2265
2266                // Print
2267                if (mDestinationSpinner.getSelectedItemId()
2268                        != DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF) {
2269                    String newText = getString(R.string.print_button);
2270                    if (!TextUtils.equals(newText, mPrintButton.getText())) {
2271                        mPrintButton.setText(R.string.print_button);
2272                    }
2273                } else {
2274                    String newText = getString(R.string.save_button);
2275                    if (!TextUtils.equals(newText, mPrintButton.getText())) {
2276                        mPrintButton.setText(R.string.save_button);
2277                    }
2278                }
2279                if ((mRangeOptionsSpinner.getSelectedItemPosition() == 1
2280                            && (TextUtils.isEmpty(mPageRangeEditText.getText()) || hasErrors()))
2281                        || (mRangeOptionsSpinner.getSelectedItemPosition() == 0
2282                            && (!mController.hasPerformedLayout() || hasErrors()))) {
2283                    mPrintButton.setEnabled(false);
2284                } else {
2285                    mPrintButton.setEnabled(true);
2286                }
2287
2288                // Copies
2289                if (mDestinationSpinner.getSelectedItemId()
2290                        != DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF) {
2291                    mCopiesEditText.setEnabled(true);
2292                } else {
2293                    mCopiesEditText.setEnabled(false);
2294                }
2295                if (mCopiesEditText.getError() == null
2296                        && TextUtils.isEmpty(mCopiesEditText.getText())) {
2297                    mIgnoreNextCopiesChange = true;
2298                    mCopiesEditText.setText(String.valueOf(MIN_COPIES));
2299                    mCopiesEditText.requestFocus();
2300                }
2301
2302                return someAttributeSelectionChanged;
2303            }
2304        }
2305
2306        private String getAdvancedOptionsActivityName(ComponentName serviceName) {
2307            PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
2308            List<PrintServiceInfo> printServices = printManager.getEnabledPrintServices();
2309            final int printServiceCount = printServices.size();
2310            for (int i = 0; i < printServiceCount; i ++) {
2311                PrintServiceInfo printServiceInfo = printServices.get(i);
2312                ServiceInfo serviceInfo = printServiceInfo.getResolveInfo().serviceInfo;
2313                if (serviceInfo.name.equals(serviceName.getClassName())
2314                        && serviceInfo.packageName.equals(serviceName.getPackageName())) {
2315                    return printServiceInfo.getAdvancedOptionsActivityName();
2316                }
2317            }
2318            return null;
2319        }
2320
2321        private void setMediaSizeSpinnerSelectionNoCallback(int position) {
2322            if (mMediaSizeSpinner.getSelectedItemPosition() != position) {
2323                mOldMediaSizeSelectionIndex = position;
2324                mMediaSizeSpinner.setSelection(position);
2325            }
2326        }
2327
2328        private void setColorModeSpinnerSelectionNoCallback(int position) {
2329            if (mColorModeSpinner.getSelectedItemPosition() != position) {
2330                mOldColorModeSelectionIndex = position;
2331                mColorModeSpinner.setSelection(position);
2332            }
2333        }
2334
2335        private void startSelectPrinterActivity() {
2336            Intent intent = new Intent(PrintJobConfigActivity.this,
2337                    SelectPrinterActivity.class);
2338            startActivityForResult(intent, ACTIVITY_REQUEST_SELECT_PRINTER);
2339        }
2340
2341        private boolean hasErrors() {
2342            if (mCopiesEditText.getError() != null) {
2343                return true;
2344            }
2345            return mPageRangeEditText.getVisibility() == View.VISIBLE
2346                    && mPageRangeEditText.getError() != null;
2347        }
2348
2349        private final class SpinnerItem<T> {
2350            final T value;
2351            CharSequence label;
2352
2353            public SpinnerItem(T value, CharSequence label) {
2354                this.value = value;
2355                this.label = label;
2356            }
2357
2358            public String toString() {
2359                return label.toString();
2360            }
2361        }
2362
2363        private final class WaitForPrinterCapabilitiesTimeout implements Runnable {
2364            private static final long GET_CAPABILITIES_TIMEOUT_MILLIS = 10000; // 10sec
2365
2366            private boolean mIsPosted;
2367
2368            public void post() {
2369                if (!mIsPosted) {
2370                    mDestinationSpinner.postDelayed(this,
2371                            GET_CAPABILITIES_TIMEOUT_MILLIS);
2372                    mIsPosted = true;
2373                }
2374            }
2375
2376            public void remove() {
2377                if (mIsPosted) {
2378                    mIsPosted = false;
2379                    mDestinationSpinner.removeCallbacks(this);
2380                }
2381            }
2382
2383            public boolean isPosted() {
2384                return mIsPosted;
2385            }
2386
2387            @Override
2388            public void run() {
2389                mIsPosted = false;
2390                if (mDestinationSpinner.getSelectedItemPosition() >= 0) {
2391                    View itemView = mDestinationSpinner.getSelectedView();
2392                    TextView titleView = (TextView) itemView.findViewById(R.id.subtitle);
2393                    try {
2394                        PackageInfo packageInfo = getPackageManager().getPackageInfo(
2395                                mCurrentPrinter.getId().getServiceName().getPackageName(), 0);
2396                        CharSequence service = packageInfo.applicationInfo.loadLabel(
2397                                getPackageManager());
2398                        String subtitle = getString(R.string.printer_unavailable, service.toString());
2399                        titleView.setText(subtitle);
2400                    } catch (NameNotFoundException nnfe) {
2401                        /* ignore */
2402                    }
2403                }
2404            }
2405        }
2406
2407        private final class DestinationAdapter extends BaseAdapter
2408                implements LoaderManager.LoaderCallbacks<List<PrinterInfo>>{
2409            private final List<PrinterInfo> mPrinters = new ArrayList<PrinterInfo>();
2410
2411            private PrinterInfo mFakePdfPrinter;
2412
2413            public DestinationAdapter() {
2414                getLoaderManager().initLoader(LOADER_ID_PRINTERS_LOADER, null, this);
2415            }
2416
2417            public int getPrinterIndex(PrinterId printerId) {
2418                for (int i = 0; i < getCount(); i++) {
2419                    PrinterInfo printer = (PrinterInfo) getItem(i);
2420                    if (printer != null && printer.getId().equals(printerId)) {
2421                        return i;
2422                    }
2423                }
2424                return AdapterView.INVALID_POSITION;
2425            }
2426
2427            public void ensurePrinterInVisibleAdapterPosition(PrinterId printerId) {
2428                final int printerCount = mPrinters.size();
2429                for (int i = 0; i < printerCount; i++) {
2430                    PrinterInfo printer = (PrinterInfo) mPrinters.get(i);
2431                    if (printer.getId().equals(printerId)) {
2432                        // If already in the list - do nothing.
2433                        if (i < getCount() - 2) {
2434                            return;
2435                        }
2436                        // Else replace the last one (two items are not printers).
2437                        final int lastPrinterIndex = getCount() - 3;
2438                        mPrinters.set(i, mPrinters.get(lastPrinterIndex));
2439                        mPrinters.set(lastPrinterIndex, printer);
2440                        notifyDataSetChanged();
2441                        return;
2442                    }
2443                }
2444            }
2445
2446            @Override
2447            public int getCount() {
2448                if (mFakePdfPrinter == null) {
2449                    return 0;
2450                }
2451                return Math.min(mPrinters.size() + 2, DEST_ADAPTER_MAX_ITEM_COUNT);
2452            }
2453
2454            @Override
2455            public boolean isEnabled(int position) {
2456                Object item = getItem(position);
2457                if (item instanceof PrinterInfo) {
2458                    PrinterInfo printer = (PrinterInfo) item;
2459                    return printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
2460                }
2461                return true;
2462            }
2463
2464            @Override
2465            public Object getItem(int position) {
2466                if (mPrinters.isEmpty()) {
2467                    if (position == 0 && mFakePdfPrinter != null) {
2468                        return mFakePdfPrinter;
2469                    }
2470                } else {
2471                    if (position < 1) {
2472                        return mPrinters.get(position);
2473                    }
2474                    if (position == 1 && mFakePdfPrinter != null) {
2475                        return mFakePdfPrinter;
2476                    }
2477                    if (position < getCount() - 1) {
2478                        return mPrinters.get(position - 1);
2479                    }
2480                }
2481                return null;
2482            }
2483
2484            @Override
2485            public long getItemId(int position) {
2486                if (mPrinters.isEmpty()) {
2487                    if (mFakePdfPrinter != null) {
2488                        if (position == 0) {
2489                            return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
2490                        } else if (position == 1) {
2491                            return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS;
2492                        }
2493                    }
2494                } else {
2495                    if (position == 1 && mFakePdfPrinter != null) {
2496                        return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
2497                    }
2498                    if (position == getCount() - 1) {
2499                        return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS;
2500                    }
2501                }
2502                return position;
2503            }
2504
2505            @Override
2506            public View getDropDownView(int position, View convertView,
2507                    ViewGroup parent) {
2508                View view = getView(position, convertView, parent);
2509                view.setEnabled(isEnabled(position));
2510                return view;
2511            }
2512
2513            @Override
2514            public View getView(int position, View convertView, ViewGroup parent) {
2515                if (convertView == null) {
2516                    convertView = getLayoutInflater().inflate(
2517                            R.layout.printer_dropdown_item, parent, false);
2518                }
2519
2520                CharSequence title = null;
2521                CharSequence subtitle = null;
2522                Drawable icon = null;
2523
2524                if (mPrinters.isEmpty()) {
2525                    if (position == 0 && mFakePdfPrinter != null) {
2526                        PrinterInfo printer = (PrinterInfo) getItem(position);
2527                        title = printer.getName();
2528                    } else if (position == 1) {
2529                        title = getString(R.string.all_printers);
2530                    }
2531                } else {
2532                    if (position == 1 && mFakePdfPrinter != null) {
2533                        PrinterInfo printer = (PrinterInfo) getItem(position);
2534                        title = printer.getName();
2535                    } else if (position == getCount() - 1) {
2536                        title = getString(R.string.all_printers);
2537                    } else {
2538                        PrinterInfo printer = (PrinterInfo) getItem(position);
2539                        title = printer.getName();
2540                        try {
2541                            PackageInfo packageInfo = getPackageManager().getPackageInfo(
2542                                    printer.getId().getServiceName().getPackageName(), 0);
2543                            subtitle = packageInfo.applicationInfo.loadLabel(getPackageManager());
2544                            icon = packageInfo.applicationInfo.loadIcon(getPackageManager());
2545                        } catch (NameNotFoundException nnfe) {
2546                            /* ignore */
2547                        }
2548                    }
2549                }
2550
2551                TextView titleView = (TextView) convertView.findViewById(R.id.title);
2552                titleView.setText(title);
2553
2554                TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle);
2555                if (!TextUtils.isEmpty(subtitle)) {
2556                    subtitleView.setText(subtitle);
2557                    subtitleView.setVisibility(View.VISIBLE);
2558                } else {
2559                    subtitleView.setText(null);
2560                    subtitleView.setVisibility(View.GONE);
2561                }
2562
2563                ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
2564                if (icon != null) {
2565                    iconView.setImageDrawable(icon);
2566                    iconView.setVisibility(View.VISIBLE);
2567                } else {
2568                    iconView.setVisibility(View.INVISIBLE);
2569                }
2570
2571                return convertView;
2572            }
2573
2574            @Override
2575            public Loader<List<PrinterInfo>> onCreateLoader(int id, Bundle args) {
2576                if (id == LOADER_ID_PRINTERS_LOADER) {
2577                    return new FusedPrintersProvider(PrintJobConfigActivity.this);
2578                }
2579                return null;
2580            }
2581
2582            @Override
2583            public void onLoadFinished(Loader<List<PrinterInfo>> loader,
2584                    List<PrinterInfo> printers) {
2585                // If this is the first load, create the fake PDF printer.
2586                // We do this to avoid flicker where the PDF printer is the
2587                // only one and as soon as the loader loads the favorites
2588                // it gets switched. Not a great user experience.
2589                if (mFakePdfPrinter == null) {
2590                    mCurrentPrinter = mFakePdfPrinter = createFakePdfPrinter();
2591                    updatePrintAttributes(mCurrentPrinter.getCapabilities());
2592                    updateUi();
2593                }
2594
2595                // We rearrange the printers if the user selects a printer
2596                // not shown in the initial short list. Therefore, we have
2597                // to keep the printer order.
2598
2599                // No old printers - do not bother keeping their position.
2600                if (mPrinters.isEmpty()) {
2601                    mPrinters.addAll(printers);
2602                    mEditor.ensureCurrentPrinterSelected();
2603                    notifyDataSetChanged();
2604                    return;
2605                }
2606
2607                // Add the new printers to a map.
2608                ArrayMap<PrinterId, PrinterInfo> newPrintersMap =
2609                        new ArrayMap<PrinterId, PrinterInfo>();
2610                final int printerCount = printers.size();
2611                for (int i = 0; i < printerCount; i++) {
2612                    PrinterInfo printer = printers.get(i);
2613                    newPrintersMap.put(printer.getId(), printer);
2614                }
2615
2616                List<PrinterInfo> newPrinters = new ArrayList<PrinterInfo>();
2617
2618                // Update printers we already have.
2619                final int oldPrinterCount = mPrinters.size();
2620                for (int i = 0; i < oldPrinterCount; i++) {
2621                    PrinterId oldPrinterId = mPrinters.get(i).getId();
2622                    PrinterInfo updatedPrinter = newPrintersMap.remove(oldPrinterId);
2623                    if (updatedPrinter != null) {
2624                        newPrinters.add(updatedPrinter);
2625                    }
2626                }
2627
2628                // Add the rest of the new printers, i.e. what is left.
2629                newPrinters.addAll(newPrintersMap.values());
2630
2631                mPrinters.clear();
2632                mPrinters.addAll(newPrinters);
2633
2634                mEditor.ensureCurrentPrinterSelected();
2635                notifyDataSetChanged();
2636            }
2637
2638            @Override
2639            public void onLoaderReset(Loader<List<PrinterInfo>> loader) {
2640                mPrinters.clear();
2641                notifyDataSetInvalidated();
2642            }
2643
2644
2645            private PrinterInfo createFakePdfPrinter() {
2646                MediaSize defaultMediaSize = MediaSizeUtils.getDefault(PrintJobConfigActivity.this);
2647
2648                PrinterId printerId = new PrinterId(getComponentName(), "PDF printer");
2649
2650                PrinterCapabilitiesInfo.Builder builder =
2651                        new PrinterCapabilitiesInfo.Builder(printerId);
2652
2653                String[] mediaSizeIds = getResources().getStringArray(
2654                        R.array.pdf_printer_media_sizes);
2655                final int mediaSizeIdCount = mediaSizeIds.length;
2656                for (int i = 0; i < mediaSizeIdCount; i++) {
2657                    String id = mediaSizeIds[i];
2658                    MediaSize mediaSize = MediaSize.getStandardMediaSizeById(id);
2659                    builder.addMediaSize(mediaSize, mediaSize.equals(defaultMediaSize));
2660                }
2661
2662                builder.addResolution(new Resolution("PDF resolution", "PDF resolution",
2663                            300, 300), true);
2664                builder.setColorModes(PrintAttributes.COLOR_MODE_COLOR
2665                        | PrintAttributes.COLOR_MODE_MONOCHROME,
2666                        PrintAttributes.COLOR_MODE_COLOR);
2667
2668                return new PrinterInfo.Builder(printerId, getString(R.string.save_as_pdf),
2669                        PrinterInfo.STATUS_IDLE)
2670                    .setCapabilities(builder.build())
2671                    .build();
2672            }
2673        }
2674    }
2675
2676    /**
2677     * An instance of this class class is intended to be the first focusable
2678     * in a layout to which the system automatically gives focus. It performs
2679     * some voodoo to avoid the first tap on it to start an edit mode, rather
2680     * to bring up the IME, i.e. to get the behavior as if the view was not
2681     * focused.
2682     */
2683    public static final class CustomEditText extends EditText {
2684        private boolean mClickedBeforeFocus;
2685        private CharSequence mError;
2686
2687        public CustomEditText(Context context, AttributeSet attrs) {
2688            super(context, attrs);
2689        }
2690
2691        @Override
2692        public boolean performClick() {
2693            super.performClick();
2694            if (isFocused() && !mClickedBeforeFocus) {
2695                clearFocus();
2696                requestFocus();
2697            }
2698            mClickedBeforeFocus = true;
2699            return true;
2700        }
2701
2702        @Override
2703        public CharSequence getError() {
2704            return mError;
2705        }
2706
2707        @Override
2708        public void setError(CharSequence error, Drawable icon) {
2709            setCompoundDrawables(null, null, icon, null);
2710            mError = error;
2711        }
2712
2713        protected void onFocusChanged(boolean gainFocus, int direction,
2714                Rect previouslyFocusedRect) {
2715            if (!gainFocus) {
2716                mClickedBeforeFocus = false;
2717            }
2718            super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
2719        }
2720    }
2721
2722    private static final class Document {
2723        public PrintDocumentInfo info;
2724        public PageRange[] pages;
2725    }
2726
2727    private static final class PageRangeUtils {
2728
2729        private static final Comparator<PageRange> sComparator = new Comparator<PageRange>() {
2730            @Override
2731            public int compare(PageRange lhs, PageRange rhs) {
2732                return lhs.getStart() - rhs.getStart();
2733            }
2734        };
2735
2736        private PageRangeUtils() {
2737            throw new UnsupportedOperationException();
2738        }
2739
2740        public static boolean contains(PageRange[] ourRanges, PageRange[] otherRanges) {
2741            if (ourRanges == null || otherRanges == null) {
2742                return false;
2743            }
2744
2745            if (ourRanges.length == 1
2746                    && PageRange.ALL_PAGES.equals(ourRanges[0])) {
2747                return true;
2748            }
2749
2750            ourRanges = normalize(ourRanges);
2751            otherRanges = normalize(otherRanges);
2752
2753            // Note that the code below relies on the ranges being normalized
2754            // which is they contain monotonically increasing non-intersecting
2755            // subranges whose start is less that or equal to the end.
2756            int otherRangeIdx = 0;
2757            final int ourRangeCount = ourRanges.length;
2758            final int otherRangeCount = otherRanges.length;
2759            for (int ourRangeIdx = 0; ourRangeIdx < ourRangeCount; ourRangeIdx++) {
2760                PageRange ourRange = ourRanges[ourRangeIdx];
2761                for (; otherRangeIdx < otherRangeCount; otherRangeIdx++) {
2762                    PageRange otherRange = otherRanges[otherRangeIdx];
2763                    if (otherRange.getStart() > ourRange.getEnd()) {
2764                        break;
2765                    }
2766                    if (otherRange.getStart() < ourRange.getStart()
2767                            || otherRange.getEnd() > ourRange.getEnd()) {
2768                        return false;
2769                    }
2770                }
2771            }
2772            if (otherRangeIdx < otherRangeCount) {
2773                return false;
2774            }
2775            return true;
2776        }
2777
2778        public static PageRange[] normalize(PageRange[] pageRanges) {
2779            if (pageRanges == null) {
2780                return null;
2781            }
2782            final int oldRangeCount = pageRanges.length;
2783            if (oldRangeCount <= 1) {
2784                return pageRanges;
2785            }
2786            Arrays.sort(pageRanges, sComparator);
2787            int newRangeCount = 1;
2788            for (int i = 0; i < oldRangeCount - 1; i++) {
2789                newRangeCount++;
2790                PageRange currentRange = pageRanges[i];
2791                PageRange nextRange = pageRanges[i + 1];
2792                if (currentRange.getEnd() + 1 >= nextRange.getStart()) {
2793                    newRangeCount--;
2794                    pageRanges[i] = null;
2795                    pageRanges[i + 1] = new PageRange(currentRange.getStart(),
2796                            Math.max(currentRange.getEnd(), nextRange.getEnd()));
2797                }
2798            }
2799            if (newRangeCount == oldRangeCount) {
2800                return pageRanges;
2801            }
2802            return Arrays.copyOfRange(pageRanges, oldRangeCount - newRangeCount,
2803                    oldRangeCount);
2804        }
2805
2806        public static void offset(PageRange[] pageRanges, int offset) {
2807            if (offset == 0) {
2808                return;
2809            }
2810            final int pageRangeCount = pageRanges.length;
2811            for (int i = 0; i < pageRangeCount; i++) {
2812                final int start = pageRanges[i].getStart() + offset;
2813                final int end = pageRanges[i].getEnd() + offset;
2814                pageRanges[i] = new PageRange(start, end);
2815            }
2816        }
2817    }
2818
2819    private static final class AutoCancellingAnimator
2820            implements OnAttachStateChangeListener, Runnable {
2821
2822        private ViewPropertyAnimator mAnimator;
2823
2824        private boolean mCancelled;
2825        private Runnable mEndCallback;
2826
2827        public static AutoCancellingAnimator animate(View view) {
2828            ViewPropertyAnimator animator = view.animate();
2829            AutoCancellingAnimator cancellingWrapper =
2830                    new AutoCancellingAnimator(animator);
2831            view.addOnAttachStateChangeListener(cancellingWrapper);
2832            return cancellingWrapper;
2833        }
2834
2835        private AutoCancellingAnimator(ViewPropertyAnimator animator) {
2836            mAnimator = animator;
2837        }
2838
2839        public AutoCancellingAnimator alpha(float alpha) {
2840            mAnimator = mAnimator.alpha(alpha);
2841            return this;
2842        }
2843
2844        public void cancel() {
2845            mAnimator.cancel();
2846        }
2847
2848        public AutoCancellingAnimator withLayer() {
2849            mAnimator = mAnimator.withLayer();
2850            return this;
2851        }
2852
2853        public AutoCancellingAnimator withEndAction(Runnable callback) {
2854            mEndCallback = callback;
2855            mAnimator = mAnimator.withEndAction(this);
2856            return this;
2857        }
2858
2859        public AutoCancellingAnimator scaleY(float scale) {
2860            mAnimator = mAnimator.scaleY(scale);
2861            return this;
2862        }
2863
2864        @Override
2865        public void onViewAttachedToWindow(View v) {
2866            /* do nothing */
2867        }
2868
2869        @Override
2870        public void onViewDetachedFromWindow(View v) {
2871            cancel();
2872        }
2873
2874        @Override
2875        public void run() {
2876            if (!mCancelled) {
2877                mEndCallback.run();
2878            }
2879        }
2880    }
2881
2882    private static final class PrintSpoolerProvider implements ServiceConnection {
2883        private final Context mContext;
2884        private final Runnable mCallback;
2885
2886        private PrintSpoolerService mSpooler;
2887
2888        public PrintSpoolerProvider(Context context, Runnable callback) {
2889            mContext = context;
2890            mCallback = callback;
2891            Intent intent = new Intent(mContext, PrintSpoolerService.class);
2892            mContext.bindService(intent, this, 0);
2893        }
2894
2895        public PrintSpoolerService getSpooler() {
2896            return mSpooler;
2897        }
2898
2899        public void destroy() {
2900            if (mSpooler != null) {
2901                mContext.unbindService(this);
2902            }
2903        }
2904
2905        @Override
2906        public void onServiceConnected(ComponentName name, IBinder service) {
2907            mSpooler = ((PrintSpoolerService.PrintSpooler) service).getService();
2908            if (mSpooler != null) {
2909                mCallback.run();
2910            }
2911        }
2912
2913        @Override
2914        public void onServiceDisconnected(ComponentName name) {
2915            /* do noting - we are in the same process */
2916        }
2917    }
2918
2919    private static final class PrintDocumentAdapterObserver
2920            extends IPrintDocumentAdapterObserver.Stub {
2921        private final WeakReference<PrintJobConfigActivity> mWeakActvity;
2922
2923        public PrintDocumentAdapterObserver(PrintJobConfigActivity activity) {
2924            mWeakActvity = new WeakReference<PrintJobConfigActivity>(activity);
2925        }
2926
2927        @Override
2928        public void onDestroy() {
2929            final PrintJobConfigActivity activity = mWeakActvity.get();
2930            if (activity != null) {
2931                activity.mController.mHandler.post(new Runnable() {
2932                    @Override
2933                    public void run() {
2934                        if (activity.mController != null) {
2935                            activity.mController.cancel();
2936                        }
2937                        if (activity.mEditor != null) {
2938                            activity.mEditor.cancel();
2939                        }
2940                        activity.finish();
2941                    }
2942                });
2943            }
2944        }
2945    }
2946}
2947