PrintManager.java revision c43639c3067dda5df189fb3cbf14f256c17e677d
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 android.print;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.app.Activity;
22import android.app.Application.ActivityLifecycleCallbacks;
23import android.content.Context;
24import android.content.IntentSender;
25import android.content.IntentSender.SendIntentException;
26import android.graphics.drawable.Icon;
27import android.os.Bundle;
28import android.os.CancellationSignal;
29import android.os.Handler;
30import android.os.ICancellationSignal;
31import android.os.Looper;
32import android.os.Message;
33import android.os.ParcelFileDescriptor;
34import android.os.RemoteException;
35import android.print.PrintDocumentAdapter.LayoutResultCallback;
36import android.print.PrintDocumentAdapter.WriteResultCallback;
37import android.printservice.PrintServiceInfo;
38import android.text.TextUtils;
39import android.util.ArrayMap;
40import android.util.Log;
41
42import com.android.internal.os.SomeArgs;
43
44import libcore.io.IoUtils;
45
46import java.lang.ref.WeakReference;
47import java.util.ArrayList;
48import java.util.Arrays;
49import java.util.Collections;
50import java.util.List;
51import java.util.Map;
52
53/**
54 * System level service for accessing the printing capabilities of the platform.
55 * <p>
56 * To obtain a handle to the print manager do the following:
57 * </p>
58 *
59 * <pre>
60 * PrintManager printManager =
61 *         (PrintManager) context.getSystemService(Context.PRINT_SERVICE);
62 * </pre>
63 *
64 * <h3>Print mechanics</h3>
65 * <p>
66 * The key idea behind printing on the platform is that the content to be printed
67 * should be laid out for the currently selected print options resulting in an
68 * optimized output and higher user satisfaction. To achieve this goal the platform
69 * declares a contract that the printing application has to follow which is defined
70 * by the {@link PrintDocumentAdapter} class. At a higher level the contract is that
71 * when the user selects some options from the print UI that may affect the way
72 * content is laid out, for example page size, the application receives a callback
73 * allowing it to layout the content to better fit these new constraints. After a
74 * layout pass the system may ask the application to render one or more pages one
75 * or more times. For example, an application may produce a single column list for
76 * smaller page sizes and a multi-column table for larger page sizes.
77 * </p>
78 * <h3>Print jobs</h3>
79 * <p>
80 * Print jobs are started by calling the {@link #print(String, PrintDocumentAdapter,
81 * PrintAttributes)} from an activity which results in bringing up the system print
82 * UI. Once the print UI is up, when the user changes a selected print option that
83 * affects the way content is laid out the system starts to interact with the
84 * application following the mechanics described the section above.
85 * </p>
86 * <p>
87 * Print jobs can be in {@link PrintJobInfo#STATE_CREATED created}, {@link
88 * PrintJobInfo#STATE_QUEUED queued}, {@link PrintJobInfo#STATE_STARTED started},
89 * {@link PrintJobInfo#STATE_BLOCKED blocked}, {@link PrintJobInfo#STATE_COMPLETED
90 * completed}, {@link PrintJobInfo#STATE_FAILED failed}, and {@link
91 * PrintJobInfo#STATE_CANCELED canceled} state. Print jobs are stored in dedicated
92 * system spooler until they are handled which is they are cancelled or completed.
93 * Active print jobs, ones that are not cancelled or completed, are considered failed
94 * if the device reboots as the new boot may be after a very long time. The user may
95 * choose to restart such print jobs. Once a print job is queued all relevant content
96 * is stored in the system spooler and its lifecycle becomes detached from this of
97 * the application that created it.
98 * </p>
99 * <p>
100 * An applications can query the print spooler for current print jobs it created
101 * but not print jobs created by other applications.
102 * </p>
103 *
104 * @see PrintJob
105 * @see PrintJobInfo
106 */
107public final class PrintManager {
108
109    private static final String LOG_TAG = "PrintManager";
110
111    private static final boolean DEBUG = false;
112
113    private static final int MSG_NOTIFY_PRINT_JOB_STATE_CHANGED = 1;
114
115    /**
116     * The action for launching the print dialog activity.
117     *
118     * @hide
119     */
120    public static final String ACTION_PRINT_DIALOG = "android.print.PRINT_DIALOG";
121
122    /**
123     * Extra with the intent for starting the print dialog.
124     * <p>
125     * <strong>Type:</strong> {@link android.content.IntentSender}
126     * </p>
127     *
128     * @hide
129     */
130    public static final String EXTRA_PRINT_DIALOG_INTENT =
131            "android.print.intent.extra.EXTRA_PRINT_DIALOG_INTENT";
132
133    /**
134     * Extra with a print job.
135     * <p>
136     * <strong>Type:</strong> {@link android.print.PrintJobInfo}
137     * </p>
138     *
139     * @hide
140     */
141    public static final String EXTRA_PRINT_JOB =
142            "android.print.intent.extra.EXTRA_PRINT_JOB";
143
144    /**
145     * Extra with the print document adapter to be printed.
146     * <p>
147     * <strong>Type:</strong> {@link android.print.IPrintDocumentAdapter}
148     * </p>
149     *
150     * @hide
151     */
152    public static final String EXTRA_PRINT_DOCUMENT_ADAPTER =
153            "android.print.intent.extra.EXTRA_PRINT_DOCUMENT_ADAPTER";
154
155    /** @hide */
156    public static final int APP_ID_ANY = -2;
157
158    private final Context mContext;
159
160    private final IPrintManager mService;
161
162    private final int mUserId;
163
164    private final int mAppId;
165
166    private final Handler mHandler;
167
168    private Map<PrintJobStateChangeListener, PrintJobStateChangeListenerWrapper> mPrintJobStateChangeListeners;
169
170    /** @hide */
171    public interface PrintJobStateChangeListener {
172
173        /**
174         * Callback notifying that a print job state changed.
175         *
176         * @param printJobId The print job id.
177         */
178        public void onPrintJobStateChanged(PrintJobId printJobId);
179    }
180
181    /**
182     * Creates a new instance.
183     *
184     * @param context The current context in which to operate.
185     * @param service The backing system service.
186     * @param userId The user id in which to operate.
187     * @param appId The application id in which to operate.
188     * @hide
189     */
190    public PrintManager(Context context, IPrintManager service, int userId, int appId) {
191        mContext = context;
192        mService = service;
193        mUserId = userId;
194        mAppId = appId;
195        mHandler = new Handler(context.getMainLooper(), null, false) {
196            @Override
197            public void handleMessage(Message message) {
198                switch (message.what) {
199                    case MSG_NOTIFY_PRINT_JOB_STATE_CHANGED: {
200                        SomeArgs args = (SomeArgs) message.obj;
201                        PrintJobStateChangeListenerWrapper wrapper =
202                                (PrintJobStateChangeListenerWrapper) args.arg1;
203                        PrintJobStateChangeListener listener = wrapper.getListener();
204                        if (listener != null) {
205                            PrintJobId printJobId = (PrintJobId) args.arg2;
206                            listener.onPrintJobStateChanged(printJobId);
207                        }
208                        args.recycle();
209                    } break;
210                }
211            }
212        };
213    }
214
215    /**
216     * Creates an instance that can access all print jobs.
217     *
218     * @param userId The user id for which to get all print jobs.
219     * @return An instance if the caller has the permission to access all print
220     *         jobs, null otherwise.
221     * @hide
222     */
223    public PrintManager getGlobalPrintManagerForUser(int userId) {
224        if (mService == null) {
225            Log.w(LOG_TAG, "Feature android.software.print not available");
226            return null;
227        }
228        return new PrintManager(mContext, mService, userId, APP_ID_ANY);
229    }
230
231    PrintJobInfo getPrintJobInfo(PrintJobId printJobId) {
232        try {
233            return mService.getPrintJobInfo(printJobId, mAppId, mUserId);
234        } catch (RemoteException re) {
235            Log.e(LOG_TAG, "Error getting a print job info:" + printJobId, re);
236        }
237        return null;
238    }
239
240    /**
241     * Adds a listener for observing the state of print jobs.
242     *
243     * @param listener The listener to add.
244     * @hide
245     */
246    public void addPrintJobStateChangeListener(PrintJobStateChangeListener listener) {
247        if (mService == null) {
248            Log.w(LOG_TAG, "Feature android.software.print not available");
249            return;
250        }
251        if (mPrintJobStateChangeListeners == null) {
252            mPrintJobStateChangeListeners = new ArrayMap<PrintJobStateChangeListener,
253                    PrintJobStateChangeListenerWrapper>();
254        }
255        PrintJobStateChangeListenerWrapper wrappedListener =
256                new PrintJobStateChangeListenerWrapper(listener, mHandler);
257        try {
258            mService.addPrintJobStateChangeListener(wrappedListener, mAppId, mUserId);
259            mPrintJobStateChangeListeners.put(listener, wrappedListener);
260        } catch (RemoteException re) {
261            Log.e(LOG_TAG, "Error adding print job state change listener", re);
262        }
263    }
264
265    /**
266     * Removes a listener for observing the state of print jobs.
267     *
268     * @param listener The listener to remove.
269     * @hide
270     */
271    public void removePrintJobStateChangeListener(PrintJobStateChangeListener listener) {
272        if (mService == null) {
273            Log.w(LOG_TAG, "Feature android.software.print not available");
274            return;
275        }
276        if (mPrintJobStateChangeListeners == null) {
277            return;
278        }
279        PrintJobStateChangeListenerWrapper wrappedListener =
280                mPrintJobStateChangeListeners.remove(listener);
281        if (wrappedListener == null) {
282            return;
283        }
284        if (mPrintJobStateChangeListeners.isEmpty()) {
285            mPrintJobStateChangeListeners = null;
286        }
287        wrappedListener.destroy();
288        try {
289            mService.removePrintJobStateChangeListener(wrappedListener, mUserId);
290        } catch (RemoteException re) {
291            Log.e(LOG_TAG, "Error removing print job state change listener", re);
292        }
293    }
294
295    /**
296     * Gets a print job given its id.
297     *
298     * @param printJobId The id of the print job.
299     * @return The print job list.
300     * @see PrintJob
301     * @hide
302     */
303    public PrintJob getPrintJob(PrintJobId printJobId) {
304        if (mService == null) {
305            Log.w(LOG_TAG, "Feature android.software.print not available");
306            return null;
307        }
308        try {
309            PrintJobInfo printJob = mService.getPrintJobInfo(printJobId, mAppId, mUserId);
310            if (printJob != null) {
311                return new PrintJob(printJob, this);
312            }
313        } catch (RemoteException re) {
314            Log.e(LOG_TAG, "Error getting print job", re);
315        }
316        return null;
317    }
318
319    /**
320     * Get the custom icon for a printer. If the icon is not cached, the icon is
321     * requested asynchronously. Once it is available the printer is updated.
322     *
323     * @param printerId the id of the printer the icon should be loaded for
324     * @return the custom icon to be used for the printer or null if the icon is
325     *         not yet available
326     * @see android.print.PrinterInfo.Builder#setHasCustomPrinterIcon()
327     * @hide
328     */
329    public Icon getCustomPrinterIcon(PrinterId printerId) {
330        if (mService == null) {
331            Log.w(LOG_TAG, "Feature android.software.print not available");
332            return null;
333        }
334        try {
335            return mService.getCustomPrinterIcon(printerId, mUserId);
336        } catch (RemoteException re) {
337            Log.e(LOG_TAG, "Error getting custom printer icon", re);
338        }
339        return null;
340    }
341
342    /**
343     * Gets the print jobs for this application.
344     *
345     * @return The print job list.
346     * @see PrintJob
347     */
348    public @NonNull List<PrintJob> getPrintJobs() {
349        if (mService == null) {
350            Log.w(LOG_TAG, "Feature android.software.print not available");
351            return Collections.emptyList();
352        }
353        try {
354            List<PrintJobInfo> printJobInfos = mService.getPrintJobInfos(mAppId, mUserId);
355            if (printJobInfos == null) {
356                return Collections.emptyList();
357            }
358            final int printJobCount = printJobInfos.size();
359            List<PrintJob> printJobs = new ArrayList<PrintJob>(printJobCount);
360            for (int i = 0; i < printJobCount; i++) {
361                printJobs.add(new PrintJob(printJobInfos.get(i), this));
362            }
363            return printJobs;
364        } catch (RemoteException re) {
365            Log.e(LOG_TAG, "Error getting print jobs", re);
366        }
367        return Collections.emptyList();
368    }
369
370    void cancelPrintJob(PrintJobId printJobId) {
371        if (mService == null) {
372            Log.w(LOG_TAG, "Feature android.software.print not available");
373            return;
374        }
375        try {
376            mService.cancelPrintJob(printJobId, mAppId, mUserId);
377        } catch (RemoteException re) {
378            Log.e(LOG_TAG, "Error canceling a print job: " + printJobId, re);
379        }
380    }
381
382    void restartPrintJob(PrintJobId printJobId) {
383        if (mService == null) {
384            Log.w(LOG_TAG, "Feature android.software.print not available");
385            return;
386        }
387        try {
388            mService.restartPrintJob(printJobId, mAppId, mUserId);
389        } catch (RemoteException re) {
390            Log.e(LOG_TAG, "Error restarting a print job: " + printJobId, re);
391        }
392    }
393
394    /**
395     * Creates a print job for printing a {@link PrintDocumentAdapter} with
396     * default print attributes.
397     * <p>
398     * Calling this method brings the print UI allowing the user to customize
399     * the print job and returns a {@link PrintJob} object without waiting for the
400     * user to customize or confirm the print job. The returned print job instance
401     * is in a {@link PrintJobInfo#STATE_CREATED created} state.
402     * <p>
403     * This method can be called only from an {@link Activity}. The rationale is that
404     * printing from a service will create an inconsistent user experience as the print
405     * UI would appear without any context.
406     * </p>
407     * <p>
408     * Also the passed in {@link PrintDocumentAdapter} will be considered invalid if
409     * your activity is finished. The rationale is that once the activity that
410     * initiated printing is finished, the provided adapter may be in an inconsistent
411     * state as it may depend on the UI presented by the activity.
412     * </p>
413     * <p>
414     * The default print attributes are a hint to the system how the data is to
415     * be printed. For example, a photo editor may look at the photo aspect ratio
416     * to determine the default orientation and provide a hint whether the printing
417     * should be in portrait or landscape. The system will do a best effort to
418     * selected the hinted options in the print dialog, given the current printer
419     * supports them.
420     * </p>
421     * <p>
422     * <strong>Note:</strong> Calling this method will bring the print dialog and
423     * the system will connect to the provided {@link PrintDocumentAdapter}. If a
424     * configuration change occurs that you application does not handle, for example
425     * a rotation change, the system will drop the connection to the adapter as the
426     * activity has to be recreated and the old adapter may be invalid in this context,
427     * hence a new adapter instance is required. As a consequence, if your activity
428     * does not handle configuration changes (default behavior), you have to save the
429     * state that you were printing and call this method again when your activity
430     * is recreated.
431     * </p>
432     *
433     * @param printJobName A name for the new print job which is shown to the user.
434     * @param documentAdapter An adapter that emits the document to print.
435     * @param attributes The default print job attributes or <code>null</code>.
436     * @return The created print job on success or null on failure.
437     * @throws IllegalStateException If not called from an {@link Activity}.
438     * @throws IllegalArgumentException If the print job name is empty or the
439     * document adapter is null.
440     *
441     * @see PrintJob
442     */
443    public @NonNull PrintJob print(@NonNull String printJobName,
444            @NonNull PrintDocumentAdapter documentAdapter,
445            @Nullable PrintAttributes attributes) {
446        if (mService == null) {
447            Log.w(LOG_TAG, "Feature android.software.print not available");
448            return null;
449        }
450        if (!(mContext instanceof Activity)) {
451            throw new IllegalStateException("Can print only from an activity");
452        }
453        if (TextUtils.isEmpty(printJobName)) {
454            throw new IllegalArgumentException("printJobName cannot be empty");
455        }
456        if (documentAdapter == null) {
457            throw new IllegalArgumentException("documentAdapter cannot be null");
458        }
459        PrintDocumentAdapterDelegate delegate = new PrintDocumentAdapterDelegate(
460                (Activity) mContext, documentAdapter);
461        try {
462            Bundle result = mService.print(printJobName, delegate,
463                    attributes, mContext.getPackageName(), mAppId, mUserId);
464            if (result != null) {
465                PrintJobInfo printJob = result.getParcelable(EXTRA_PRINT_JOB);
466                IntentSender intent = result.getParcelable(EXTRA_PRINT_DIALOG_INTENT);
467                if (printJob == null || intent == null) {
468                    return null;
469                }
470                try {
471                    mContext.startIntentSender(intent, null, 0, 0, 0);
472                    return new PrintJob(printJob, this);
473                } catch (SendIntentException sie) {
474                    Log.e(LOG_TAG, "Couldn't start print job config activity.", sie);
475                }
476            }
477        } catch (RemoteException re) {
478            Log.e(LOG_TAG, "Error creating a print job", re);
479        }
480        return null;
481    }
482
483    /**
484     * Gets the list of enabled print services.
485     *
486     * @return The enabled service list or an empty list.
487     * @hide
488     */
489    public List<PrintServiceInfo> getEnabledPrintServices() {
490        if (mService == null) {
491            Log.w(LOG_TAG, "Feature android.software.print not available");
492            return Collections.emptyList();
493        }
494        try {
495            List<PrintServiceInfo> enabledServices = mService.getEnabledPrintServices(mUserId);
496            if (enabledServices != null) {
497                return enabledServices;
498            }
499        } catch (RemoteException re) {
500            Log.e(LOG_TAG, "Error getting the enabled print services", re);
501        }
502        return Collections.emptyList();
503    }
504
505    /**
506     * Gets the list of installed print services.
507     *
508     * @return The installed service list or an empty list.
509     * @hide
510     */
511    public List<PrintServiceInfo> getInstalledPrintServices() {
512        if (mService == null) {
513            Log.w(LOG_TAG, "Feature android.software.print not available");
514            return Collections.emptyList();
515        }
516        try {
517            List<PrintServiceInfo> installedServices = mService.getInstalledPrintServices(mUserId);
518            if (installedServices != null) {
519                return installedServices;
520            }
521        } catch (RemoteException re) {
522            Log.e(LOG_TAG, "Error getting the installed print services", re);
523        }
524        return Collections.emptyList();
525    }
526
527    /**
528     * @hide
529     */
530    public PrinterDiscoverySession createPrinterDiscoverySession() {
531        if (mService == null) {
532            Log.w(LOG_TAG, "Feature android.software.print not available");
533            return null;
534        }
535        return new PrinterDiscoverySession(mService, mContext, mUserId);
536    }
537
538    private static final class PrintDocumentAdapterDelegate extends IPrintDocumentAdapter.Stub
539            implements ActivityLifecycleCallbacks {
540        private final Object mLock = new Object();
541
542        private Activity mActivity; // Strong reference OK - cleared in destroy
543
544        private PrintDocumentAdapter mDocumentAdapter; // Strong reference OK - cleared in destroy
545
546        private Handler mHandler; // Strong reference OK - cleared in destroy
547
548        private IPrintDocumentAdapterObserver mObserver; // Strong reference OK - cleared in destroy
549
550        private DestroyableCallback mPendingCallback;
551
552        public PrintDocumentAdapterDelegate(Activity activity,
553                PrintDocumentAdapter documentAdapter) {
554            mActivity = activity;
555            mDocumentAdapter = documentAdapter;
556            mHandler = new MyHandler(mActivity.getMainLooper());
557            mActivity.getApplication().registerActivityLifecycleCallbacks(this);
558        }
559
560        @Override
561        public void setObserver(IPrintDocumentAdapterObserver observer) {
562            final boolean destroyed;
563            synchronized (mLock) {
564                mObserver = observer;
565                destroyed = isDestroyedLocked();
566            }
567
568            if (destroyed && observer != null) {
569                try {
570                    observer.onDestroy();
571                } catch (RemoteException re) {
572                    Log.e(LOG_TAG, "Error announcing destroyed state", re);
573                }
574            }
575        }
576
577        @Override
578        public void start() {
579            synchronized (mLock) {
580                // If destroyed the handler is null.
581                if (!isDestroyedLocked()) {
582                    mHandler.obtainMessage(MyHandler.MSG_ON_START,
583                            mDocumentAdapter).sendToTarget();
584                }
585            }
586        }
587
588        @Override
589        public void layout(PrintAttributes oldAttributes, PrintAttributes newAttributes,
590                ILayoutResultCallback callback, Bundle metadata, int sequence) {
591
592            ICancellationSignal cancellationTransport = CancellationSignal.createTransport();
593            try {
594                callback.onLayoutStarted(cancellationTransport, sequence);
595            } catch (RemoteException re) {
596                // The spooler is dead - can't recover.
597                Log.e(LOG_TAG, "Error notifying for layout start", re);
598                return;
599            }
600
601            synchronized (mLock) {
602                // If destroyed the handler is null.
603                if (isDestroyedLocked()) {
604                    return;
605                }
606
607                CancellationSignal cancellationSignal = CancellationSignal.fromTransport(
608                        cancellationTransport);
609
610                SomeArgs args = SomeArgs.obtain();
611                args.arg1 = mDocumentAdapter;
612                args.arg2 = oldAttributes;
613                args.arg3 = newAttributes;
614                args.arg4 = cancellationSignal;
615                args.arg5 = new MyLayoutResultCallback(callback, sequence);
616                args.arg6 = metadata;
617
618                mHandler.obtainMessage(MyHandler.MSG_ON_LAYOUT, args).sendToTarget();
619            }
620        }
621
622        @Override
623        public void write(PageRange[] pages, ParcelFileDescriptor fd,
624                IWriteResultCallback callback, int sequence) {
625
626            ICancellationSignal cancellationTransport = CancellationSignal.createTransport();
627            try {
628                callback.onWriteStarted(cancellationTransport, sequence);
629            } catch (RemoteException re) {
630                // The spooler is dead - can't recover.
631                Log.e(LOG_TAG, "Error notifying for write start", re);
632                return;
633            }
634
635            synchronized (mLock) {
636                // If destroyed the handler is null.
637                if (isDestroyedLocked()) {
638                    return;
639                }
640
641                CancellationSignal cancellationSignal = CancellationSignal.fromTransport(
642                        cancellationTransport);
643
644                SomeArgs args = SomeArgs.obtain();
645                args.arg1 = mDocumentAdapter;
646                args.arg2 = pages;
647                args.arg3 = fd;
648                args.arg4 = cancellationSignal;
649                args.arg5 = new MyWriteResultCallback(callback, fd, sequence);
650
651                mHandler.obtainMessage(MyHandler.MSG_ON_WRITE, args).sendToTarget();
652            }
653        }
654
655        @Override
656        public void finish() {
657            synchronized (mLock) {
658                // If destroyed the handler is null.
659                if (!isDestroyedLocked()) {
660                    mHandler.obtainMessage(MyHandler.MSG_ON_FINISH,
661                            mDocumentAdapter).sendToTarget();
662                }
663            }
664        }
665
666        @Override
667        public void kill(String reason) {
668            synchronized (mLock) {
669                // If destroyed the handler is null.
670                if (!isDestroyedLocked()) {
671                    mHandler.obtainMessage(MyHandler.MSG_ON_KILL,
672                            reason).sendToTarget();
673                }
674            }
675        }
676
677        @Override
678        public void onActivityPaused(Activity activity) {
679            /* do nothing */
680        }
681
682        @Override
683        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
684            /* do nothing */
685        }
686
687        @Override
688        public void onActivityStarted(Activity activity) {
689            /* do nothing */
690        }
691
692        @Override
693        public void onActivityResumed(Activity activity) {
694            /* do nothing */
695        }
696
697        @Override
698        public void onActivityStopped(Activity activity) {
699            /* do nothing */
700        }
701
702        @Override
703        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
704            /* do nothing */
705        }
706
707        @Override
708        public void onActivityDestroyed(Activity activity) {
709            // We really care only if the activity is being destroyed to
710            // notify the the print spooler so it can close the print dialog.
711            // Note the the spooler has a death recipient that observes if
712            // this process gets killed so we cover the case of onDestroy not
713            // being called due to this process being killed to reclaim memory.
714            IPrintDocumentAdapterObserver observer = null;
715            synchronized (mLock) {
716                if (activity == mActivity) {
717                    observer = mObserver;
718                    destroyLocked();
719                }
720            }
721            if (observer != null) {
722                try {
723                    observer.onDestroy();
724                } catch (RemoteException re) {
725                    Log.e(LOG_TAG, "Error announcing destroyed state", re);
726                }
727            }
728        }
729
730        private boolean isDestroyedLocked() {
731            return (mActivity == null);
732        }
733
734        private void destroyLocked() {
735            mActivity.getApplication().unregisterActivityLifecycleCallbacks(
736                    PrintDocumentAdapterDelegate.this);
737            mActivity = null;
738
739            mDocumentAdapter = null;
740
741            // This method is only called from the main thread, so
742            // clearing the messages guarantees that any time a
743            // message is handled we are not in a destroyed state.
744            mHandler.removeMessages(MyHandler.MSG_ON_START);
745            mHandler.removeMessages(MyHandler.MSG_ON_LAYOUT);
746            mHandler.removeMessages(MyHandler.MSG_ON_WRITE);
747            mHandler.removeMessages(MyHandler.MSG_ON_FINISH);
748            mHandler = null;
749
750            mObserver = null;
751
752            if (mPendingCallback != null) {
753                mPendingCallback.destroy();
754                mPendingCallback = null;
755            }
756        }
757
758        private final class MyHandler extends Handler {
759            public static final int MSG_ON_START = 1;
760            public static final int MSG_ON_LAYOUT = 2;
761            public static final int MSG_ON_WRITE = 3;
762            public static final int MSG_ON_FINISH = 4;
763            public static final int MSG_ON_KILL = 5;
764
765            public MyHandler(Looper looper) {
766                super(looper, null, true);
767            }
768
769            @Override
770            public void handleMessage(Message message) {
771                switch (message.what) {
772                    case MSG_ON_START: {
773                        if (DEBUG) {
774                            Log.i(LOG_TAG, "onStart()");
775                        }
776
777                        ((PrintDocumentAdapter) message.obj).onStart();
778                    } break;
779
780                    case MSG_ON_LAYOUT: {
781                        SomeArgs args = (SomeArgs) message.obj;
782                        PrintDocumentAdapter adapter = (PrintDocumentAdapter) args.arg1;
783                        PrintAttributes oldAttributes = (PrintAttributes) args.arg2;
784                        PrintAttributes newAttributes = (PrintAttributes) args.arg3;
785                        CancellationSignal cancellation = (CancellationSignal) args.arg4;
786                        LayoutResultCallback callback = (LayoutResultCallback) args.arg5;
787                        Bundle metadata = (Bundle) args.arg6;
788                        args.recycle();
789
790                        if (DEBUG) {
791                            StringBuilder builder = new StringBuilder();
792                            builder.append("PrintDocumentAdapter#onLayout() {\n");
793                            builder.append("\n  oldAttributes:").append(oldAttributes);
794                            builder.append("\n  newAttributes:").append(newAttributes);
795                            builder.append("\n  preview:").append(metadata.getBoolean(
796                                    PrintDocumentAdapter.EXTRA_PRINT_PREVIEW));
797                            builder.append("\n}");
798                            Log.i(LOG_TAG, builder.toString());
799                        }
800
801                        adapter.onLayout(oldAttributes, newAttributes, cancellation,
802                                callback, metadata);
803                    } break;
804
805                    case MSG_ON_WRITE: {
806                        SomeArgs args = (SomeArgs) message.obj;
807                        PrintDocumentAdapter adapter = (PrintDocumentAdapter) args.arg1;
808                        PageRange[] pages = (PageRange[]) args.arg2;
809                        ParcelFileDescriptor fd = (ParcelFileDescriptor) args.arg3;
810                        CancellationSignal cancellation = (CancellationSignal) args.arg4;
811                        WriteResultCallback callback = (WriteResultCallback) args.arg5;
812                        args.recycle();
813
814                        if (DEBUG) {
815                            StringBuilder builder = new StringBuilder();
816                            builder.append("PrintDocumentAdapter#onWrite() {\n");
817                            builder.append("\n  pages:").append(Arrays.toString(pages));
818                            builder.append("\n}");
819                            Log.i(LOG_TAG, builder.toString());
820                        }
821
822                        adapter.onWrite(pages, fd, cancellation, callback);
823                    } break;
824
825                    case MSG_ON_FINISH: {
826                        if (DEBUG) {
827                            Log.i(LOG_TAG, "onFinish()");
828                        }
829
830                        ((PrintDocumentAdapter) message.obj).onFinish();
831
832                        // Done printing, so destroy this instance as it
833                        // should not be used anymore.
834                        synchronized (mLock) {
835                            destroyLocked();
836                        }
837                    } break;
838
839                    case MSG_ON_KILL: {
840                        if (DEBUG) {
841                            Log.i(LOG_TAG, "onKill()");
842                        }
843
844                        String reason = (String) message.obj;
845                        throw new RuntimeException(reason);
846                    }
847
848                    default: {
849                        throw new IllegalArgumentException("Unknown message: "
850                                + message.what);
851                    }
852                }
853            }
854        }
855
856        private interface DestroyableCallback {
857            public void destroy();
858        }
859
860        private final class MyLayoutResultCallback extends LayoutResultCallback
861                implements DestroyableCallback {
862            private ILayoutResultCallback mCallback;
863            private final int mSequence;
864
865            public MyLayoutResultCallback(ILayoutResultCallback callback,
866                    int sequence) {
867                mCallback = callback;
868                mSequence = sequence;
869            }
870
871            @Override
872            public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {
873                final ILayoutResultCallback callback;
874                synchronized (mLock) {
875                    callback = mCallback;
876                }
877
878                // If the callback is null we are destroyed.
879                if (callback == null) {
880                    Log.e(LOG_TAG, "PrintDocumentAdapter is destroyed. Did you "
881                            + "finish the printing activity before print completion "
882                            + "or did you invoke a callback after finish?");
883                    return;
884                }
885
886                try {
887                    if (info == null) {
888                        throw new NullPointerException("document info cannot be null");
889                    }
890
891                    try {
892                        callback.onLayoutFinished(info, changed, mSequence);
893                    } catch (RemoteException re) {
894                        Log.e(LOG_TAG, "Error calling onLayoutFinished", re);
895                    }
896                } finally {
897                    destroy();
898                }
899            }
900
901            @Override
902            public void onLayoutFailed(CharSequence error) {
903                final ILayoutResultCallback callback;
904                synchronized (mLock) {
905                    callback = mCallback;
906                }
907
908                // If the callback is null we are destroyed.
909                if (callback == null) {
910                    Log.e(LOG_TAG, "PrintDocumentAdapter is destroyed. Did you "
911                            + "finish the printing activity before print completion "
912                            + "or did you invoke a callback after finish?");
913                    return;
914                }
915
916                try {
917                    callback.onLayoutFailed(error, mSequence);
918                } catch (RemoteException re) {
919                    Log.e(LOG_TAG, "Error calling onLayoutFailed", re);
920                } finally {
921                    destroy();
922                }
923            }
924
925            @Override
926            public void onLayoutCancelled() {
927                final ILayoutResultCallback callback;
928                synchronized (mLock) {
929                    callback = mCallback;
930                }
931
932                // If the callback is null we are destroyed.
933                if (callback == null) {
934                    Log.e(LOG_TAG, "PrintDocumentAdapter is destroyed. Did you "
935                            + "finish the printing activity before print completion "
936                            + "or did you invoke a callback after finish?");
937                    return;
938                }
939
940                try {
941                    callback.onLayoutCanceled(mSequence);
942                } catch (RemoteException re) {
943                    Log.e(LOG_TAG, "Error calling onLayoutFailed", re);
944                } finally {
945                    destroy();
946                }
947            }
948
949            @Override
950            public void destroy() {
951                synchronized (mLock) {
952                    mCallback = null;
953                    mPendingCallback = null;
954                }
955            }
956        }
957
958        private final class MyWriteResultCallback extends WriteResultCallback
959                implements DestroyableCallback {
960            private ParcelFileDescriptor mFd;
961            private IWriteResultCallback mCallback;
962            private final int mSequence;
963
964            public MyWriteResultCallback(IWriteResultCallback callback,
965                    ParcelFileDescriptor fd, int sequence) {
966                mFd = fd;
967                mSequence = sequence;
968                mCallback = callback;
969            }
970
971            @Override
972            public void onWriteFinished(PageRange[] pages) {
973                final IWriteResultCallback callback;
974                synchronized (mLock) {
975                    callback = mCallback;
976                }
977
978                // If the callback is null we are destroyed.
979                if (callback == null) {
980                    Log.e(LOG_TAG, "PrintDocumentAdapter is destroyed. Did you "
981                            + "finish the printing activity before print completion "
982                            + "or did you invoke a callback after finish?");
983                    return;
984                }
985
986                try {
987                    if (pages == null) {
988                        throw new IllegalArgumentException("pages cannot be null");
989                    }
990                    if (pages.length == 0) {
991                        throw new IllegalArgumentException("pages cannot be empty");
992                    }
993
994                    try {
995                        callback.onWriteFinished(pages, mSequence);
996                    } catch (RemoteException re) {
997                        Log.e(LOG_TAG, "Error calling onWriteFinished", re);
998                    }
999                } finally {
1000                    destroy();
1001                }
1002            }
1003
1004            @Override
1005            public void onWriteFailed(CharSequence error) {
1006                final IWriteResultCallback callback;
1007                synchronized (mLock) {
1008                    callback = mCallback;
1009                }
1010
1011                // If the callback is null we are destroyed.
1012                if (callback == null) {
1013                    Log.e(LOG_TAG, "PrintDocumentAdapter is destroyed. Did you "
1014                            + "finish the printing activity before print completion "
1015                            + "or did you invoke a callback after finish?");
1016                    return;
1017                }
1018
1019                try {
1020                    callback.onWriteFailed(error, mSequence);
1021                } catch (RemoteException re) {
1022                    Log.e(LOG_TAG, "Error calling onWriteFailed", re);
1023                } finally {
1024                    destroy();
1025                }
1026            }
1027
1028            @Override
1029            public void onWriteCancelled() {
1030                final IWriteResultCallback callback;
1031                synchronized (mLock) {
1032                    callback = mCallback;
1033                }
1034
1035                // If the callback is null we are destroyed.
1036                if (callback == null) {
1037                    Log.e(LOG_TAG, "PrintDocumentAdapter is destroyed. Did you "
1038                            + "finish the printing activity before print completion "
1039                            + "or did you invoke a callback after finish?");
1040                    return;
1041                }
1042
1043                try {
1044                    callback.onWriteCanceled(mSequence);
1045                } catch (RemoteException re) {
1046                    Log.e(LOG_TAG, "Error calling onWriteCanceled", re);
1047                } finally {
1048                    destroy();
1049                }
1050            }
1051
1052            @Override
1053            public void destroy() {
1054                synchronized (mLock) {
1055                    IoUtils.closeQuietly(mFd);
1056                    mCallback = null;
1057                    mFd = null;
1058                    mPendingCallback = null;
1059                }
1060            }
1061        }
1062    }
1063
1064    private static final class PrintJobStateChangeListenerWrapper extends
1065            IPrintJobStateChangeListener.Stub {
1066        private final WeakReference<PrintJobStateChangeListener> mWeakListener;
1067        private final WeakReference<Handler> mWeakHandler;
1068
1069        public PrintJobStateChangeListenerWrapper(PrintJobStateChangeListener listener,
1070                Handler handler) {
1071            mWeakListener = new WeakReference<PrintJobStateChangeListener>(listener);
1072            mWeakHandler = new WeakReference<Handler>(handler);
1073        }
1074
1075        @Override
1076        public void onPrintJobStateChanged(PrintJobId printJobId) {
1077            Handler handler = mWeakHandler.get();
1078            PrintJobStateChangeListener listener = mWeakListener.get();
1079            if (handler != null && listener != null) {
1080                SomeArgs args = SomeArgs.obtain();
1081                args.arg1 = this;
1082                args.arg2 = printJobId;
1083                handler.obtainMessage(MSG_NOTIFY_PRINT_JOB_STATE_CHANGED,
1084                        args).sendToTarget();
1085            }
1086        }
1087
1088        public void destroy() {
1089            mWeakListener.clear();
1090        }
1091
1092        public PrintJobStateChangeListener getListener() {
1093            return mWeakListener.get();
1094        }
1095    }
1096}
1097