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