PrinterDiscoverySession.java revision d26d4898fcc9b78f4b66118895c375384098205e
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#updatePrinters(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 = 100;
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 ArrayMap<PrinterId, PrinterInfo> mLastSentPrinters;
93
94    private IPrintServiceClient mObserver;
95
96    private boolean mIsDestroyed;
97
98    private boolean mIsDiscoveryStarted;
99
100    /**
101     * Constructor.
102     */
103    public PrinterDiscoverySession() {
104        mId = sIdCounter++;
105    }
106
107    void setObserver(IPrintServiceClient observer) {
108        mObserver = observer;
109        // If some printers were added in the method that
110        // created the session, send them over.
111        if (!mPrinters.isEmpty()) {
112            sendAddedPrinters(mObserver, getPrinters());
113        }
114    }
115
116    int getId() {
117        return mId;
118    }
119
120    /**
121     * Gets the printers reported in this session. For example, if you add two
122     * printers and remove one of them, the returned list will contain only
123     * the printer that was added but not removed.
124     * <p>
125     * <strong>Note: </strong> Calls to this method after the session is
126     * destroyed, that is after the {@link #onDestroy()} callback, will be ignored.
127     * </p>
128     *
129     * @return The printers.
130     *
131     * @see #addPrinters(List)
132     * @see #removePrinters(List)
133     * @see #updatePrinters(List)
134     * @see #isDestroyed()
135     */
136    public final List<PrinterInfo> getPrinters() {
137        PrintService.throwIfNotCalledOnMainThread();
138        if (mIsDestroyed) {
139            return Collections.emptyList();
140        }
141        return new ArrayList<PrinterInfo>(mPrinters.values());
142    }
143
144    /**
145     * Adds discovered printers. Adding an already added printer has no effect.
146     * Removed printers can be added again. You can call this method multiple
147     * times during the life of this session. Duplicates will be ignored.
148     * <p>
149     * <strong>Note: </strong> Calls to this method after the session is
150     * destroyed, that is after the {@link #onDestroy()} callback, will be ignored.
151     * </p>
152     *
153     * @param printers The printers to add.
154     *
155     * @see #removePrinters(List)
156     * @see #updatePrinters(List)
157     * @see #getPrinters()
158     * @see #isDestroyed()
159     */
160    public final void addPrinters(List<PrinterInfo> printers) {
161        PrintService.throwIfNotCalledOnMainThread();
162
163        // If the session is destroyed - nothing do to.
164        if (mIsDestroyed) {
165            Log.w(LOG_TAG, "Not adding printers - session destroyed.");
166            return;
167        }
168
169        if (mIsDiscoveryStarted) {
170            // If during discovery, add the new printers and send them.
171            List<PrinterInfo> addedPrinters = new ArrayList<PrinterInfo>();
172            final int addedPrinterCount = printers.size();
173            for (int i = 0; i < addedPrinterCount; i++) {
174                PrinterInfo addedPrinter = printers.get(i);
175                if (mPrinters.get(addedPrinter.getId()) == null) {
176                    mPrinters.put(addedPrinter.getId(), addedPrinter);
177                    addedPrinters.add(addedPrinter);
178                }
179            }
180
181            // Send the added printers, if such.
182            if (!addedPrinters.isEmpty()) {
183                sendAddedPrinters(mObserver, addedPrinters);
184            }
185        } else {
186            // Remember the last sent printers if needed.
187            if (mLastSentPrinters == null) {
188                mLastSentPrinters = new ArrayMap<PrinterId, PrinterInfo>(mPrinters);
189            }
190
191            // Update the printers.
192            final int addedPrinterCount = printers.size();
193            for (int i = 0; i < addedPrinterCount; i++) {
194                PrinterInfo addedPrinter = printers.get(i);
195                if (mPrinters.get(addedPrinter.getId()) == null) {
196                    mPrinters.put(addedPrinter.getId(), addedPrinter);
197                }
198            }
199        }
200    }
201
202    private static void sendAddedPrinters(IPrintServiceClient observer,
203        List<PrinterInfo> printers) {
204        try {
205            final int printerCount = printers.size();
206            if (printerCount <= MAX_ITEMS_PER_CALLBACK) {
207                observer.onPrintersAdded(printers);
208            } else {
209                // Send the added printers in chunks avoiding the binder transaction limit.
210                final int transactionCount = (printerCount / MAX_ITEMS_PER_CALLBACK) + 1;
211                for (int i = 0; i < transactionCount; i++) {
212                    final int start = i * MAX_ITEMS_PER_CALLBACK;
213                    final int end = Math.min(start + MAX_ITEMS_PER_CALLBACK, printerCount);
214                    List<PrinterInfo> subPrinters = printers.subList(start, end);
215                    observer.onPrintersAdded(subPrinters);
216                }
217            }
218        } catch (RemoteException re) {
219            Log.e(LOG_TAG, "Error sending added printers", re);
220        }
221    }
222
223    /**
224     * Removes added printers. Removing an already removed or never added
225     * printer has no effect. Removed printers can be added again. You can
226     * call this method multiple times during the lifetime of this session.
227     * <p>
228     * <strong>Note: </strong> Calls to this method after the session is
229     * destroyed, that is after the {@link #onDestroy()} callback, will be ignored.
230     * </p>
231     *
232     * @param printerIds The ids of the removed printers.
233     *
234     * @see #addPrinters(List)
235     * @see #updatePrinters(List)
236     * @see #getPrinters()
237     * @see #isDestroyed()
238     */
239    public final void removePrinters(List<PrinterId> printerIds) {
240        PrintService.throwIfNotCalledOnMainThread();
241
242        // If the session is destroyed - nothing do to.
243        if (mIsDestroyed) {
244            Log.w(LOG_TAG, "Not removing printers - session destroyed.");
245            return;
246        }
247
248        if (mIsDiscoveryStarted) {
249            // If during discovery, remove existing printers and send them.
250            List<PrinterId> removedPrinterIds = new ArrayList<PrinterId>();
251            final int removedPrinterIdCount = printerIds.size();
252            for (int i = 0; i < removedPrinterIdCount; i++) {
253                PrinterId removedPrinterId = printerIds.get(i);
254                if (mPrinters.remove(removedPrinterId) != null) {
255                    removedPrinterIds.add(removedPrinterId);
256                }
257            }
258
259            // Send the removed printers, if such.
260            if (!removedPrinterIds.isEmpty()) {
261                sendRemovedPrinters(mObserver, removedPrinterIds);
262            }
263        } else {
264            // Remember the last sent printers if needed.
265            if (mLastSentPrinters == null) {
266                mLastSentPrinters = new ArrayMap<PrinterId, PrinterInfo>(mPrinters);
267            }
268
269            // Update the printers.
270            final int removedPrinterIdCount = printerIds.size();
271            for (int i = 0; i < removedPrinterIdCount; i++) {
272                PrinterId removedPrinterId = printerIds.get(i);
273                mPrinters.remove(removedPrinterId);
274            }
275        }
276    }
277
278    private static void sendRemovedPrinters(IPrintServiceClient observer,
279            List<PrinterId> printerIds) {
280        try {
281            final int printerIdCount = printerIds.size();
282            if (printerIdCount <= MAX_ITEMS_PER_CALLBACK) {
283                observer.onPrintersRemoved(printerIds);
284            } else {
285                final int transactionCount = (printerIdCount / MAX_ITEMS_PER_CALLBACK) + 1;
286                for (int i = 0; i < transactionCount; i++) {
287                    final int start = i * MAX_ITEMS_PER_CALLBACK;
288                    final int end = Math.min(start + MAX_ITEMS_PER_CALLBACK, printerIdCount);
289                    List<PrinterId> subPrinterIds = printerIds.subList(start, end);
290                    observer.onPrintersRemoved(subPrinterIds);
291                }
292            }
293        } catch (RemoteException re) {
294            Log.e(LOG_TAG, "Error sending removed printers", re);
295        }
296    }
297
298    /**
299     * Updates added printers. Updating a printer that was not added or that
300     * was removed has no effect. You can call this method multiple times
301     * during the lifetime of this session.
302     * <p>
303     * <strong>Note: </strong> Calls to this method after the session is
304     * destroyed, that is after the {@link #onDestroy()} callback, will be ignored.
305     * </p>
306     *
307     * @param printers The printers to update.
308     *
309     * @see #addPrinters(List)
310     * @see #removePrinters(List)
311     * @see #getPrinters()
312     * @see #isDestroyed()
313     */
314    public final void updatePrinters(List<PrinterInfo> printers) {
315        PrintService.throwIfNotCalledOnMainThread();
316
317        // If the session is destroyed - nothing do to.
318        if (mIsDestroyed) {
319            Log.w(LOG_TAG, "Not updating printers - session destroyed.");
320            return;
321        }
322
323        if (mIsDiscoveryStarted) {
324            // If during discovery, update existing printers and send them.
325            List<PrinterInfo> updatedPrinters = new ArrayList<PrinterInfo>();
326            final int updatedPrinterCount = printers.size();
327            for (int i = 0; i < updatedPrinterCount; i++) {
328                PrinterInfo updatedPrinter = printers.get(i);
329                PrinterInfo oldPrinter = mPrinters.get(updatedPrinter.getId());
330                if (oldPrinter != null && !oldPrinter.equals(updatedPrinter)) {
331                    mPrinters.put(updatedPrinter.getId(), updatedPrinter);
332                    updatedPrinters.add(updatedPrinter);
333                }
334            }
335
336            // Send the updated printers, if such.
337            if (!updatedPrinters.isEmpty()) {
338                sendUpdatedPrinters(mObserver, updatedPrinters);
339            }
340        } else {
341            // Remember the last sent printers if needed.
342            if (mLastSentPrinters == null) {
343                mLastSentPrinters = new ArrayMap<PrinterId, PrinterInfo>(mPrinters);
344            }
345
346            // Update the printers.
347            final int updatedPrinterCount = printers.size();
348            for (int i = 0; i < updatedPrinterCount; i++) {
349                PrinterInfo updatedPrinter = printers.get(i);
350                PrinterInfo oldPrinter = mPrinters.get(updatedPrinter.getId());
351                if (oldPrinter != null && !oldPrinter.equals(updatedPrinter)) {
352                    mPrinters.put(updatedPrinter.getId(), updatedPrinter);
353                }
354            }
355        }
356    }
357
358    private static void sendUpdatedPrinters(IPrintServiceClient observer,
359            List<PrinterInfo> printers) {
360        try {
361            final int printerCount = printers.size();
362            if (printerCount <= MAX_ITEMS_PER_CALLBACK) {
363                observer.onPrintersUpdated(printers);
364            } else {
365                final int transactionCount = (printerCount / MAX_ITEMS_PER_CALLBACK) + 1;
366                for (int i = 0; i < transactionCount; i++) {
367                    final int start = i * MAX_ITEMS_PER_CALLBACK;
368                    final int end = Math.min(start + MAX_ITEMS_PER_CALLBACK, printerCount);
369                    List<PrinterInfo> subPrinters = printers.subList(start, end);
370                    observer.onPrintersUpdated(subPrinters);
371                }
372            }
373        } catch (RemoteException re) {
374            Log.e(LOG_TAG, "Error sending updated printers", re);
375        }
376    }
377
378    private void sendOutOfDiscoveryPeriodPrinterChanges() {
379        // Noting changed since the last discovery period - nothing to do.
380        if (mLastSentPrinters == null || mLastSentPrinters.isEmpty()) {
381            mLastSentPrinters = null;
382            return;
383        }
384
385        List<PrinterInfo> addedPrinters = null;
386        List<PrinterInfo> updatedPrinters = null;
387        List<PrinterId> removedPrinterIds = null;
388
389        // Determine the added and updated printers.
390        for (PrinterInfo printer : mPrinters.values()) {
391            PrinterInfo sentPrinter = mLastSentPrinters.get(printer.getId());
392            if (sentPrinter != null) {
393                if (!sentPrinter.equals(printer)) {
394                    if (updatedPrinters == null) {
395                        updatedPrinters = new ArrayList<PrinterInfo>();
396                    }
397                    updatedPrinters.add(printer);
398                }
399            } else {
400                if (addedPrinters == null) {
401                    addedPrinters = new ArrayList<PrinterInfo>();
402                }
403                addedPrinters.add(printer);
404            }
405        }
406
407        // Send the added printers, if such.
408        if (addedPrinters != null) {
409            sendAddedPrinters(mObserver, addedPrinters);
410        }
411
412        // Send the updated printers, if such.
413        if (updatedPrinters != null) {
414            sendUpdatedPrinters(mObserver, updatedPrinters);
415        }
416
417        // Determine the removed printers.
418        for (PrinterInfo sentPrinter : mLastSentPrinters.values()) {
419            if (!mPrinters.containsKey(sentPrinter.getId())) {
420                if (removedPrinterIds == null) {
421                    removedPrinterIds = new ArrayList<PrinterId>();
422                }
423                removedPrinterIds.add(sentPrinter.getId());
424            }
425        }
426
427        // Send the removed printers, if such.
428        if (removedPrinterIds != null) {
429            sendRemovedPrinters(mObserver, removedPrinterIds);
430        }
431
432        mLastSentPrinters = null;
433    }
434
435    /**
436     * Callback asking you to start printer discovery. Discovered printers should be
437     * added via calling {@link #addPrinters(List)}. Added printers that disappeared
438     * should be removed via calling {@link #removePrinters(List)}. Added printers
439     * whose properties or capabilities changed should be updated via calling {@link
440     * #updatePrinters(List)}. You will receive a call to call to {@link
441     * #onStopPrinterDiscovery()} when you should stop printer discovery.
442     * <p>
443     * During the lifetime of this session all printers that are known to your print
444     * service have to be added. The system does not retain any printers across sessions.
445     * However, if you were asked to start and then stop performing printer discovery
446     * in this session, then a subsequent discovering should not re-discover already
447     * discovered printers.
448     * </p>
449     * <p>
450     * <strong>Note: </strong>You are also given a list of printers whose availability
451     * has to be checked first. For example, these printers could be the user's favorite
452     * ones, therefore they have to be verified first. You do <strong>not need</strong>
453     * to provide the capabilities of the printers, rather verify whether they exist
454     * similarly to {@link #onValidatePrinters(List)}.
455     * </p>
456     *
457     * @param priorityList The list of printers to validate first. Never null.
458     *
459     * @see #onStopPrinterDiscovery()
460     * @see #addPrinters(List)
461     * @see #removePrinters(List)
462     * @see #updatePrinters(List)
463     * @see #isPrinterDiscoveryStarted()
464     */
465    public abstract void onStartPrinterDiscovery(List<PrinterId> priorityList);
466
467    /**
468     * Callback notifying you that you should stop printer discovery.
469     *
470     * @see #onStartPrinterDiscovery(List)
471     * @see #isPrinterDiscoveryStarted()
472     */
473    public abstract void onStopPrinterDiscovery();
474
475    /**
476     * Callback asking you to validate that the given printers are valid, that
477     * is they exist. You are responsible for checking whether these printers
478     * exist and for the ones that do exist notify the system via calling
479     * {@link #updatePrinters(List)}.
480     * <p>
481     * <strong>Note: </strong> You are <strong>not required</strong> to provide
482     * the printer capabilities when updating the printers that do exist.
483     * <p>
484     *
485     * @param printerIds The printers to validate.
486     *
487     * @see #updatePrinters(List)
488     * @see PrinterInfo.Builder#setCapabilities(PrinterCapabilitiesInfo)
489     *      PrinterInfo.Builder.setCapabilities(PrinterCapabilitiesInfo)
490     */
491    public abstract void onValidatePrinters(List<PrinterId> printerIds);
492
493    /**
494     * Callback asking you to start tracking the state of a printer. Tracking
495     * the state means that you should do a best effort to observe the state
496     * of this printer and notify the system if that state changes via calling
497     * {@link #updatePrinters(List)}.
498     * <p>
499     * <strong>Note: </strong> A printer can be initially added without its
500     * capabilities to avoid polling printers that the user will not select.
501     * However, after this method is called you are expected to update the
502     * printer <strong>including</strong> its capabilities. Otherwise, the
503     * printer will be ignored.
504     * <p>
505     * <p>
506     * A scenario when you may be requested to track a printer's state is if
507     * the user selects that printer and the system has to present print
508     * options UI based on the printer's capabilities. In this case the user
509     * should be promptly informed if, for example, the printer becomes
510     * unavailable.
511     * </p>
512     *
513     * @param printerId The printer to start tracking.
514     *
515     * @see #onStopPrinterStateTracking(PrinterId)
516     * @see #updatePrinters(List)
517     * @see PrinterInfo.Builder#setCapabilities(PrinterCapabilitiesInfo)
518     *      PrinterInfo.Builder.setCapabilities(PrinterCapabilitiesInfo)
519     */
520    public abstract void onStartPrinterStateTracking(PrinterId printerId);
521
522    /**
523     * Callback asking you to stop tracking the state of a printer. The passed
524     * in printer id is the one for which you received a call to {@link
525     * #onStartPrinterStateTracking(PrinterId)}.
526     *
527     * @param printerId The printer to stop tracking.
528     *
529     * @see #onStartPrinterStateTracking(PrinterId)
530     */
531    public abstract void onStopPrinterStateTracking(PrinterId printerId);
532
533    /**
534     * Notifies you that the session is destroyed. After this callback is invoked
535     * any calls to the methods of this class will be ignored, {@link #isDestroyed()}
536     * will return true and you will also no longer receive callbacks.
537     *
538     * @see #isDestroyed()
539     */
540    public abstract void onDestroy();
541
542    /**
543     * Gets whether the session is destroyed.
544     *
545     * @return Whether the session is destroyed.
546     *
547     * @see #onDestroy()
548     */
549    public final boolean isDestroyed() {
550        PrintService.throwIfNotCalledOnMainThread();
551        return mIsDestroyed;
552    }
553
554    /**
555     * Gets whether printer discovery is started.
556     *
557     * @return Whether printer discovery is destroyed.
558     *
559     * @see #onStartPrinterDiscovery(List)
560     * @see #onStopPrinterDiscovery()
561     */
562    public final boolean isPrinterDiscoveryStarted() {
563        PrintService.throwIfNotCalledOnMainThread();
564        return mIsDiscoveryStarted;
565    }
566
567    void startPrinterDiscovery(List<PrinterId> priorityList) {
568        if (!mIsDestroyed) {
569            mIsDiscoveryStarted = true;
570            sendOutOfDiscoveryPeriodPrinterChanges();
571            if (priorityList == null) {
572                priorityList = Collections.emptyList();
573            }
574            onStartPrinterDiscovery(priorityList);
575        }
576    }
577
578    void stopPrinterDiscovery() {
579        if (!mIsDestroyed) {
580            mIsDiscoveryStarted = false;
581            onStopPrinterDiscovery();
582        }
583    }
584
585    void validatePrinters(List<PrinterId> printerIds) {
586        if (!mIsDestroyed && mObserver != null) {
587            onValidatePrinters(printerIds);
588        }
589    }
590
591    void startPrinterStateTracking(PrinterId printerId) {
592        if (!mIsDestroyed && mObserver != null) {
593            onStartPrinterStateTracking(printerId);
594        }
595    }
596
597    void stopPrinterStateTracking(PrinterId printerId) {
598        if (!mIsDestroyed && mObserver != null) {
599            onStopPrinterStateTracking(printerId);
600        }
601    }
602
603    void destroy() {
604        if (!mIsDestroyed) {
605            mIsDestroyed = true;
606            mIsDiscoveryStarted = false;
607            mPrinters.clear();
608            mLastSentPrinters = null;
609            mObserver = null;
610            onDestroy();
611        }
612    }
613}
614