PrinterDiscoverySession.java revision 773f54de3de9bce7b6f915aa47ed686b161d77aa
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.printservice;
18
19import android.os.RemoteException;
20import android.print.PrinterCapabilitiesInfo;
21import android.print.PrinterId;
22import android.print.PrinterInfo;
23import android.util.ArrayMap;
24import android.util.Log;
25
26import java.util.ArrayList;
27import java.util.Collections;
28import java.util.List;
29
30/**
31 * This class encapsulates the interaction between a print service and the
32 * system during printer discovery. During printer discovery you are responsible
33 * for adding discovered printers, removing already added printers that
34 * disappeared, and updating already added printers.
35 * <p>
36 * During the lifetime of this session you may be asked to start and stop
37 * performing printer discovery multiple times. You will receive a call to {@link
38 * PrinterDiscoverySession#onStartPrinterDiscovery(List)} to start printer
39 * discovery and a call to {@link PrinterDiscoverySession#onStopPrinterDiscovery()}
40 * to stop printer discovery. When the system is no longer interested in printers
41 * discovered by this session you will receive a call to {@link #onDestroy()} at
42 * which point the system will no longer call into the session and all the session
43 * methods will do nothing.
44 * </p>
45 * <p>
46 * Discovered printers are added by invoking {@link
47 * PrinterDiscoverySession#addPrinters(List)}. Added printers that disappeared are
48 * removed by invoking {@link PrinterDiscoverySession#removePrinters(List)}. Added
49 * printers whose properties or capabilities changed are updated through a call to
50 * {@link PrinterDiscoverySession#addPrinters(List)}. The printers added in this
51 * session can be acquired via {@link #getPrinters()} where the returned printers
52 * will be an up-to-date snapshot of the printers that you reported during the
53 * session. Printers are <strong>not</strong> persisted across sessions.
54 * </p>
55 * <p>
56 * The system will make a call to {@link #onValidatePrinters(List)} if you
57 * need to update some printers. It is possible that you add a printer without
58 * specifying its capabilities. This enables you to avoid querying all discovered
59 * printers for their capabilities, rather querying the capabilities of a printer
60 * only if necessary. For example, the system will request that you update a printer
61 * if it gets selected by the user. When validating printers you do not need to
62 * provide the printers' capabilities but may do so.
63 * </p>
64 * <p>
65 * If the system is interested in being constantly updated for the state of a
66 * printer you will receive a call to {@link #onStartPrinterStateTracking(PrinterId)}
67 * after which you will have to do a best effort to keep the system updated for
68 * changes in the printer state and capabilities. You also <strong>must</strong>
69 * update the printer capabilities if you did not provide them when adding it, or
70 * the printer will be ignored. When the system is no longer interested in getting
71 * updates for a printer you will receive a call to {@link #onStopPrinterStateTracking(
72 * PrinterId)}.
73 * </p>
74 * <p>
75 * <strong>Note: </strong> All callbacks in this class are executed on the main
76 * application thread. You also have to invoke any method of this class on the main
77 * application thread.
78 * </p>
79 */
80public abstract class PrinterDiscoverySession {
81    private static final String LOG_TAG = "PrinterDiscoverySession";
82
83    private static final int MAX_ITEMS_PER_CALLBACK = 50;
84
85    private static int sIdCounter = 0;
86
87    private final int mId;
88
89    private final ArrayMap<PrinterId, PrinterInfo> mPrinters =
90            new ArrayMap<PrinterId, PrinterInfo>();
91
92    private final List<PrinterId> mTrackedPrinters =
93            new ArrayList<PrinterId>();
94
95    private ArrayMap<PrinterId, PrinterInfo> mLastSentPrinters;
96
97    private IPrintServiceClient mObserver;
98
99    private boolean mIsDestroyed;
100
101    private boolean mIsDiscoveryStarted;
102
103    /**
104     * Constructor.
105     */
106    public PrinterDiscoverySession() {
107        mId = sIdCounter++;
108    }
109
110    void setObserver(IPrintServiceClient observer) {
111        mObserver = observer;
112        // If some printers were added in the method that
113        // created the session, send them over.
114        if (!mPrinters.isEmpty()) {
115            sendAddedPrinters(mObserver, getPrinters());
116        }
117    }
118
119    int getId() {
120        return mId;
121    }
122
123    /**
124     * Gets the printers reported in this session. For example, if you add two
125     * printers and remove one of them, the returned list will contain only
126     * the printer that was added but not removed.
127     * <p>
128     * <strong>Note: </strong> Calls to this method after the session is
129     * destroyed, that is after the {@link #onDestroy()} callback, will be ignored.
130     * </p>
131     *
132     * @return The printers.
133     *
134     * @see #addPrinters(List)
135     * @see #removePrinters(List)
136     * @see #isDestroyed()
137     */
138    public final List<PrinterInfo> getPrinters() {
139        PrintService.throwIfNotCalledOnMainThread();
140        if (mIsDestroyed) {
141            return Collections.emptyList();
142        }
143        return new ArrayList<PrinterInfo>(mPrinters.values());
144    }
145
146    /**
147     * Adds discovered printers. Adding an already added printer updates it.
148     * Removed printers can be added again. You can call this method multiple
149     * times during the life of this session. Duplicates will be ignored.
150     * <p>
151     * <strong>Note: </strong> Calls to this method after the session is
152     * destroyed, that is after the {@link #onDestroy()} callback, will be ignored.
153     * </p>
154     *
155     * @param printers The printers to add.
156     *
157     * @see #removePrinters(List)
158     * @see #getPrinters()
159     * @see #isDestroyed()
160     */
161    public final void addPrinters(List<PrinterInfo> printers) {
162        PrintService.throwIfNotCalledOnMainThread();
163
164        // If the session is destroyed - nothing do to.
165        if (mIsDestroyed) {
166            Log.w(LOG_TAG, "Not adding printers - session destroyed.");
167            return;
168        }
169
170        if (mIsDiscoveryStarted) {
171            // If during discovery, add the new printers and send them.
172            List<PrinterInfo> addedPrinters = null;
173            final int addedPrinterCount = printers.size();
174            for (int i = 0; i < addedPrinterCount; i++) {
175                PrinterInfo addedPrinter = printers.get(i);
176                PrinterInfo oldPrinter = mPrinters.put(addedPrinter.getId(), addedPrinter);
177                if (oldPrinter == null || !oldPrinter.equals(addedPrinter)) {
178                    if (addedPrinters == null) {
179                        addedPrinters = new ArrayList<PrinterInfo>();
180                    }
181                    addedPrinters.add(addedPrinter);
182                }
183            }
184
185            // Send the added printers, if such.
186            if (addedPrinters != null) {
187                sendAddedPrinters(mObserver, addedPrinters);
188            }
189        } else {
190            // Remember the last sent printers if needed.
191            if (mLastSentPrinters == null) {
192                mLastSentPrinters = new ArrayMap<PrinterId, PrinterInfo>(mPrinters);
193            }
194
195            // Update the printers.
196            final int addedPrinterCount = printers.size();
197            for (int i = 0; i < addedPrinterCount; i++) {
198                PrinterInfo addedPrinter = printers.get(i);
199                if (mPrinters.get(addedPrinter.getId()) == null) {
200                    mPrinters.put(addedPrinter.getId(), addedPrinter);
201                }
202            }
203        }
204    }
205
206    private static void sendAddedPrinters(IPrintServiceClient observer,
207        List<PrinterInfo> printers) {
208        try {
209            final int printerCount = printers.size();
210            if (printerCount <= MAX_ITEMS_PER_CALLBACK) {
211                observer.onPrintersAdded(printers);
212            } else {
213                // Send the added printers in chunks avoiding the binder transaction limit.
214                final int transactionCount = (printerCount / MAX_ITEMS_PER_CALLBACK) + 1;
215                for (int i = 0; i < transactionCount; i++) {
216                    final int start = i * MAX_ITEMS_PER_CALLBACK;
217                    final int end = Math.min(start + MAX_ITEMS_PER_CALLBACK, printerCount);
218                    List<PrinterInfo> subPrinters = printers.subList(start, end);
219                    observer.onPrintersAdded(subPrinters);
220                }
221            }
222        } catch (RemoteException re) {
223            Log.e(LOG_TAG, "Error sending added printers", re);
224        }
225    }
226
227    /**
228     * Removes added printers. Removing an already removed or never added
229     * printer has no effect. Removed printers can be added again. You can
230     * call this method multiple times during the lifetime of this session.
231     * <p>
232     * <strong>Note: </strong> Calls to this method after the session is
233     * destroyed, that is after the {@link #onDestroy()} callback, will be ignored.
234     * </p>
235     *
236     * @param printerIds The ids of the removed printers.
237     *
238     * @see #addPrinters(List)
239     * @see #getPrinters()
240     * @see #isDestroyed()
241     */
242    public final void removePrinters(List<PrinterId> printerIds) {
243        PrintService.throwIfNotCalledOnMainThread();
244
245        // If the session is destroyed - nothing do to.
246        if (mIsDestroyed) {
247            Log.w(LOG_TAG, "Not removing printers - session destroyed.");
248            return;
249        }
250
251        if (mIsDiscoveryStarted) {
252            // If during discovery, remove existing printers and send them.
253            List<PrinterId> removedPrinterIds = new ArrayList<PrinterId>();
254            final int removedPrinterIdCount = printerIds.size();
255            for (int i = 0; i < removedPrinterIdCount; i++) {
256                PrinterId removedPrinterId = printerIds.get(i);
257                if (mPrinters.remove(removedPrinterId) != null) {
258                    removedPrinterIds.add(removedPrinterId);
259                }
260            }
261
262            // Send the removed printers, if such.
263            if (!removedPrinterIds.isEmpty()) {
264                sendRemovedPrinters(mObserver, removedPrinterIds);
265            }
266        } else {
267            // Remember the last sent printers if needed.
268            if (mLastSentPrinters == null) {
269                mLastSentPrinters = new ArrayMap<PrinterId, PrinterInfo>(mPrinters);
270            }
271
272            // Update the printers.
273            final int removedPrinterIdCount = printerIds.size();
274            for (int i = 0; i < removedPrinterIdCount; i++) {
275                PrinterId removedPrinterId = printerIds.get(i);
276                mPrinters.remove(removedPrinterId);
277            }
278        }
279    }
280
281    private static void sendRemovedPrinters(IPrintServiceClient observer,
282            List<PrinterId> printerIds) {
283        try {
284            final int printerIdCount = printerIds.size();
285            if (printerIdCount <= MAX_ITEMS_PER_CALLBACK) {
286                observer.onPrintersRemoved(printerIds);
287            } else {
288                final int transactionCount = (printerIdCount / MAX_ITEMS_PER_CALLBACK) + 1;
289                for (int i = 0; i < transactionCount; i++) {
290                    final int start = i * MAX_ITEMS_PER_CALLBACK;
291                    final int end = Math.min(start + MAX_ITEMS_PER_CALLBACK, printerIdCount);
292                    List<PrinterId> subPrinterIds = printerIds.subList(start, end);
293                    observer.onPrintersRemoved(subPrinterIds);
294                }
295            }
296        } catch (RemoteException re) {
297            Log.e(LOG_TAG, "Error sending removed printers", re);
298        }
299    }
300
301    private void sendOutOfDiscoveryPeriodPrinterChanges() {
302        // Noting changed since the last discovery period - nothing to do.
303        if (mLastSentPrinters == null || mLastSentPrinters.isEmpty()) {
304            mLastSentPrinters = null;
305            return;
306        }
307
308        // Determine the added printers.
309        List<PrinterInfo> addedPrinters = null;
310        for (PrinterInfo printer : mPrinters.values()) {
311            PrinterInfo sentPrinter = mLastSentPrinters.get(printer.getId());
312            if (sentPrinter == null || !sentPrinter.equals(printer)) {
313                if (addedPrinters == null) {
314                    addedPrinters = new ArrayList<PrinterInfo>();
315                }
316                addedPrinters.add(printer);
317            }
318        }
319
320        // Send the added printers, if such.
321        if (addedPrinters != null) {
322            sendAddedPrinters(mObserver, addedPrinters);
323        }
324
325        // Determine the removed printers.
326        List<PrinterId> removedPrinterIds = null;
327        for (PrinterInfo sentPrinter : mLastSentPrinters.values()) {
328            if (!mPrinters.containsKey(sentPrinter.getId())) {
329                if (removedPrinterIds == null) {
330                    removedPrinterIds = new ArrayList<PrinterId>();
331                }
332                removedPrinterIds.add(sentPrinter.getId());
333            }
334        }
335
336        // Send the removed printers, if such.
337        if (removedPrinterIds != null) {
338            sendRemovedPrinters(mObserver, removedPrinterIds);
339        }
340
341        mLastSentPrinters = null;
342    }
343
344    /**
345     * Callback asking you to start printer discovery. Discovered printers should be
346     * added via calling {@link #addPrinters(List)}. Added printers that disappeared
347     * should be removed via calling {@link #removePrinters(List)}. Added printers
348     * whose properties or capabilities changed should be updated via calling {@link
349     * #addPrinters(List)}. You will receive a call to {@link #onStopPrinterDiscovery()}
350     * when you should stop printer discovery.
351     * <p>
352     * During the lifetime of this session all printers that are known to your print
353     * service have to be added. The system does not retain any printers across sessions.
354     * However, if you were asked to start and then stop performing printer discovery
355     * in this session, then a subsequent discovering should not re-discover already
356     * discovered printers. You can get the printers reported during this session by
357     * calling {@link #getPrinters()}.
358     * </p>
359     * <p>
360     * <strong>Note: </strong>You are also given a list of printers whose availability
361     * has to be checked first. For example, these printers could be the user's favorite
362     * ones, therefore they have to be verified first. You do <strong>not need</strong>
363     * to provide the capabilities of the printers, rather verify whether they exist
364     * similarly to {@link #onValidatePrinters(List)}.
365     * </p>
366     *
367     * @param priorityList The list of printers to validate first. Never null.
368     *
369     * @see #onStopPrinterDiscovery()
370     * @see #addPrinters(List)
371     * @see #removePrinters(List)
372     * @see #isPrinterDiscoveryStarted()
373     */
374    public abstract void onStartPrinterDiscovery(List<PrinterId> priorityList);
375
376    /**
377     * Callback notifying you that you should stop printer discovery.
378     *
379     * @see #onStartPrinterDiscovery(List)
380     * @see #isPrinterDiscoveryStarted()
381     */
382    public abstract void onStopPrinterDiscovery();
383
384    /**
385     * Callback asking you to validate that the given printers are valid, that
386     * is they exist. You are responsible for checking whether these printers
387     * exist and for the ones that do exist notify the system via calling
388     * {@link #addPrinters(List)}.
389     * <p>
390     * <strong>Note: </strong> You are <strong>not required</strong> to provide
391     * the printer capabilities when updating the printers that do exist.
392     * <p>
393     *
394     * @param printerIds The printers to validate.
395     *
396     * @see PrinterInfo.Builder#setCapabilities(PrinterCapabilitiesInfo)
397     *      PrinterInfo.Builder.setCapabilities(PrinterCapabilitiesInfo)
398     */
399    public abstract void onValidatePrinters(List<PrinterId> printerIds);
400
401    /**
402     * Callback asking you to start tracking the state of a printer. Tracking
403     * the state means that you should do a best effort to observe the state
404     * of this printer and notify the system if that state changes via calling
405     * {@link #addPrinters(List)}.
406     * <p>
407     * <strong>Note: </strong> A printer can be initially added without its
408     * capabilities to avoid polling printers that the user will not select.
409     * However, after this method is called you are expected to update the
410     * printer <strong>including</strong> its capabilities. Otherwise, the
411     * printer will be ignored.
412     * <p>
413     * <p>
414     * A scenario when you may be requested to track a printer's state is if
415     * the user selects that printer and the system has to present print
416     * options UI based on the printer's capabilities. In this case the user
417     * should be promptly informed if, for example, the printer becomes
418     * unavailable.
419     * </p>
420     *
421     * @param printerId The printer to start tracking.
422     *
423     * @see #onStopPrinterStateTracking(PrinterId)
424     * @see PrinterInfo.Builder#setCapabilities(PrinterCapabilitiesInfo)
425     *      PrinterInfo.Builder.setCapabilities(PrinterCapabilitiesInfo)
426     */
427    public abstract void onStartPrinterStateTracking(PrinterId printerId);
428
429    /**
430     * Callback asking you to stop tracking the state of a printer. The passed
431     * in printer id is the one for which you received a call to {@link
432     * #onStartPrinterStateTracking(PrinterId)}.
433     *
434     * @param printerId The printer to stop tracking.
435     *
436     * @see #onStartPrinterStateTracking(PrinterId)
437     */
438    public abstract void onStopPrinterStateTracking(PrinterId printerId);
439
440    /**
441     * Gets the printers that should be tracked. These are printers that are
442     * important to the user and for which you received a call to {@link
443     * #onStartPrinterStateTracking(PrinterId)} asking you to observer their
444     * state and reporting it to the system via {@link #addPrinters(List)}.
445     * You will receive a call to {@link #onStopPrinterStateTracking(PrinterId)}
446     * if you should stop tracking a printer.
447     * <p>
448     * <strong>Note: </strong> Calls to this method after the session is
449     * destroyed, that is after the {@link #onDestroy()} callback, will be ignored.
450     * </p>
451     *
452     * @return The printers.
453     *
454     * @see #onStartPrinterStateTracking(PrinterId)
455     * @see #onStopPrinterStateTracking(PrinterId)
456     * @see #isDestroyed()
457     */
458    public final List<PrinterId> getTrackedPrinters() {
459        PrintService.throwIfNotCalledOnMainThread();
460        if (mIsDestroyed) {
461            return Collections.emptyList();
462        }
463        return new ArrayList<PrinterId>(mTrackedPrinters);
464    }
465
466    /**
467     * Notifies you that the session is destroyed. After this callback is invoked
468     * any calls to the methods of this class will be ignored, {@link #isDestroyed()}
469     * will return true and you will also no longer receive callbacks.
470     *
471     * @see #isDestroyed()
472     */
473    public abstract void onDestroy();
474
475    /**
476     * Gets whether the session is destroyed.
477     *
478     * @return Whether the session is destroyed.
479     *
480     * @see #onDestroy()
481     */
482    public final boolean isDestroyed() {
483        PrintService.throwIfNotCalledOnMainThread();
484        return mIsDestroyed;
485    }
486
487    /**
488     * Gets whether printer discovery is started.
489     *
490     * @return Whether printer discovery is destroyed.
491     *
492     * @see #onStartPrinterDiscovery(List)
493     * @see #onStopPrinterDiscovery()
494     */
495    public final boolean isPrinterDiscoveryStarted() {
496        PrintService.throwIfNotCalledOnMainThread();
497        return mIsDiscoveryStarted;
498    }
499
500    void startPrinterDiscovery(List<PrinterId> priorityList) {
501        if (!mIsDestroyed) {
502            mIsDiscoveryStarted = true;
503            sendOutOfDiscoveryPeriodPrinterChanges();
504            if (priorityList == null) {
505                priorityList = Collections.emptyList();
506            }
507            onStartPrinterDiscovery(priorityList);
508        }
509    }
510
511    void stopPrinterDiscovery() {
512        if (!mIsDestroyed) {
513            mIsDiscoveryStarted = false;
514            onStopPrinterDiscovery();
515        }
516    }
517
518    void validatePrinters(List<PrinterId> printerIds) {
519        if (!mIsDestroyed && mObserver != null) {
520            onValidatePrinters(printerIds);
521        }
522    }
523
524    void startPrinterStateTracking(PrinterId printerId) {
525        if (!mIsDestroyed && mObserver != null
526                && !mTrackedPrinters.contains(printerId)) {
527            mTrackedPrinters.add(printerId);
528            onStartPrinterStateTracking(printerId);
529        }
530    }
531
532    void stopPrinterStateTracking(PrinterId printerId) {
533        if (!mIsDestroyed && mObserver != null
534                && mTrackedPrinters.remove(printerId)) {
535            onStopPrinterStateTracking(printerId);
536        }
537    }
538
539    void destroy() {
540        if (!mIsDestroyed) {
541            mIsDestroyed = true;
542            mIsDiscoveryStarted = false;
543            mPrinters.clear();
544            mLastSentPrinters = null;
545            mObserver = null;
546            onDestroy();
547        }
548    }
549}
550