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