/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.printservice;
import android.os.RemoteException;
import android.print.PrinterCapabilitiesInfo;
import android.print.PrinterId;
import android.print.PrinterInfo;
import android.util.ArrayMap;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* This class encapsulates the interaction between a print service and the
* system during printer discovery. During printer discovery you are responsible
* for adding discovered printers, removing already added printers that
* disappeared, and updating already added printers.
*
* During the lifetime of this session you may be asked to start and stop
* performing printer discovery multiple times. You will receive a call to {@link
* PrinterDiscoverySession#onStartPrinterDiscovery(List)} to start printer
* discovery and a call to {@link PrinterDiscoverySession#onStopPrinterDiscovery()}
* to stop printer discovery. When the system is no longer interested in printers
* discovered by this session you will receive a call to {@link #onDestroy()} at
* which point the system will no longer call into the session and all the session
* methods will do nothing.
*
*
* Discovered printers are added by invoking {@link
* PrinterDiscoverySession#addPrinters(List)}. Added printers that disappeared are
* removed by invoking {@link PrinterDiscoverySession#removePrinters(List)}. Added
* printers whose properties or capabilities changed are updated through a call to
* {@link PrinterDiscoverySession#addPrinters(List)}. The printers added in this
* session can be acquired via {@link #getPrinters()} where the returned printers
* will be an up-to-date snapshot of the printers that you reported during the
* session. Printers are not persisted across sessions.
*
*
* The system will make a call to {@link #onValidatePrinters(List)} if you
* need to update some printers. It is possible that you add a printer without
* specifying its capabilities. This enables you to avoid querying all discovered
* printers for their capabilities, rather querying the capabilities of a printer
* only if necessary. For example, the system will request that you update a printer
* if it gets selected by the user. When validating printers you do not need to
* provide the printers' capabilities but may do so.
*
*
* If the system is interested in being constantly updated for the state of a
* printer you will receive a call to {@link #onStartPrinterStateTracking(PrinterId)}
* after which you will have to do a best effort to keep the system updated for
* changes in the printer state and capabilities. You also must
* update the printer capabilities if you did not provide them when adding it, or
* the printer will be ignored. When the system is no longer interested in getting
* updates for a printer you will receive a call to {@link #onStopPrinterStateTracking(
* PrinterId)}.
*
*
* Note: All callbacks in this class are executed on the main
* application thread. You also have to invoke any method of this class on the main
* application thread.
*
*/
public abstract class PrinterDiscoverySession {
private static final String LOG_TAG = "PrinterDiscoverySession";
private static final int MAX_ITEMS_PER_CALLBACK = 50;
private static int sIdCounter = 0;
private final int mId;
private final ArrayMap mPrinters =
new ArrayMap();
private final List mTrackedPrinters =
new ArrayList();
private ArrayMap mLastSentPrinters;
private IPrintServiceClient mObserver;
private boolean mIsDestroyed;
private boolean mIsDiscoveryStarted;
/**
* Constructor.
*/
public PrinterDiscoverySession() {
mId = sIdCounter++;
}
void setObserver(IPrintServiceClient observer) {
mObserver = observer;
// If some printers were added in the method that
// created the session, send them over.
if (!mPrinters.isEmpty()) {
sendAddedPrinters(mObserver, getPrinters());
}
}
int getId() {
return mId;
}
/**
* Gets the printers reported in this session. For example, if you add two
* printers and remove one of them, the returned list will contain only
* the printer that was added but not removed.
*
* Note: Calls to this method after the session is
* destroyed, that is after the {@link #onDestroy()} callback, will be ignored.
*
*
* @return The printers.
*
* @see #addPrinters(List)
* @see #removePrinters(List)
* @see #isDestroyed()
*/
public final List getPrinters() {
PrintService.throwIfNotCalledOnMainThread();
if (mIsDestroyed) {
return Collections.emptyList();
}
return new ArrayList(mPrinters.values());
}
/**
* Adds discovered printers. Adding an already added printer updates it.
* Removed printers can be added again. You can call this method multiple
* times during the life of this session. Duplicates will be ignored.
*
* Note: Calls to this method after the session is
* destroyed, that is after the {@link #onDestroy()} callback, will be ignored.
*
*
* @param printers The printers to add.
*
* @see #removePrinters(List)
* @see #getPrinters()
* @see #isDestroyed()
*/
public final void addPrinters(List printers) {
PrintService.throwIfNotCalledOnMainThread();
// If the session is destroyed - nothing do to.
if (mIsDestroyed) {
Log.w(LOG_TAG, "Not adding printers - session destroyed.");
return;
}
if (mIsDiscoveryStarted) {
// If during discovery, add the new printers and send them.
List addedPrinters = null;
final int addedPrinterCount = printers.size();
for (int i = 0; i < addedPrinterCount; i++) {
PrinterInfo addedPrinter = printers.get(i);
PrinterInfo oldPrinter = mPrinters.put(addedPrinter.getId(), addedPrinter);
if (oldPrinter == null || !oldPrinter.equals(addedPrinter)) {
if (addedPrinters == null) {
addedPrinters = new ArrayList();
}
addedPrinters.add(addedPrinter);
}
}
// Send the added printers, if such.
if (addedPrinters != null) {
sendAddedPrinters(mObserver, addedPrinters);
}
} else {
// Remember the last sent printers if needed.
if (mLastSentPrinters == null) {
mLastSentPrinters = new ArrayMap(mPrinters);
}
// Update the printers.
final int addedPrinterCount = printers.size();
for (int i = 0; i < addedPrinterCount; i++) {
PrinterInfo addedPrinter = printers.get(i);
if (mPrinters.get(addedPrinter.getId()) == null) {
mPrinters.put(addedPrinter.getId(), addedPrinter);
}
}
}
}
private static void sendAddedPrinters(IPrintServiceClient observer,
List printers) {
try {
final int printerCount = printers.size();
if (printerCount <= MAX_ITEMS_PER_CALLBACK) {
observer.onPrintersAdded(printers);
} else {
// Send the added printers in chunks avoiding the binder transaction limit.
final int transactionCount = (printerCount / MAX_ITEMS_PER_CALLBACK) + 1;
for (int i = 0; i < transactionCount; i++) {
final int start = i * MAX_ITEMS_PER_CALLBACK;
final int end = Math.min(start + MAX_ITEMS_PER_CALLBACK, printerCount);
List subPrinters = printers.subList(start, end);
observer.onPrintersAdded(subPrinters);
}
}
} catch (RemoteException re) {
Log.e(LOG_TAG, "Error sending added printers", re);
}
}
/**
* Removes added printers. Removing an already removed or never added
* printer has no effect. Removed printers can be added again. You can
* call this method multiple times during the lifetime of this session.
*
* Note: Calls to this method after the session is
* destroyed, that is after the {@link #onDestroy()} callback, will be ignored.
*
*
* @param printerIds The ids of the removed printers.
*
* @see #addPrinters(List)
* @see #getPrinters()
* @see #isDestroyed()
*/
public final void removePrinters(List printerIds) {
PrintService.throwIfNotCalledOnMainThread();
// If the session is destroyed - nothing do to.
if (mIsDestroyed) {
Log.w(LOG_TAG, "Not removing printers - session destroyed.");
return;
}
if (mIsDiscoveryStarted) {
// If during discovery, remove existing printers and send them.
List removedPrinterIds = new ArrayList();
final int removedPrinterIdCount = printerIds.size();
for (int i = 0; i < removedPrinterIdCount; i++) {
PrinterId removedPrinterId = printerIds.get(i);
if (mPrinters.remove(removedPrinterId) != null) {
removedPrinterIds.add(removedPrinterId);
}
}
// Send the removed printers, if such.
if (!removedPrinterIds.isEmpty()) {
sendRemovedPrinters(mObserver, removedPrinterIds);
}
} else {
// Remember the last sent printers if needed.
if (mLastSentPrinters == null) {
mLastSentPrinters = new ArrayMap(mPrinters);
}
// Update the printers.
final int removedPrinterIdCount = printerIds.size();
for (int i = 0; i < removedPrinterIdCount; i++) {
PrinterId removedPrinterId = printerIds.get(i);
mPrinters.remove(removedPrinterId);
}
}
}
private static void sendRemovedPrinters(IPrintServiceClient observer,
List printerIds) {
try {
final int printerIdCount = printerIds.size();
if (printerIdCount <= MAX_ITEMS_PER_CALLBACK) {
observer.onPrintersRemoved(printerIds);
} else {
final int transactionCount = (printerIdCount / MAX_ITEMS_PER_CALLBACK) + 1;
for (int i = 0; i < transactionCount; i++) {
final int start = i * MAX_ITEMS_PER_CALLBACK;
final int end = Math.min(start + MAX_ITEMS_PER_CALLBACK, printerIdCount);
List subPrinterIds = printerIds.subList(start, end);
observer.onPrintersRemoved(subPrinterIds);
}
}
} catch (RemoteException re) {
Log.e(LOG_TAG, "Error sending removed printers", re);
}
}
private void sendOutOfDiscoveryPeriodPrinterChanges() {
// Noting changed since the last discovery period - nothing to do.
if (mLastSentPrinters == null || mLastSentPrinters.isEmpty()) {
mLastSentPrinters = null;
return;
}
// Determine the added printers.
List addedPrinters = null;
for (PrinterInfo printer : mPrinters.values()) {
PrinterInfo sentPrinter = mLastSentPrinters.get(printer.getId());
if (sentPrinter == null || !sentPrinter.equals(printer)) {
if (addedPrinters == null) {
addedPrinters = new ArrayList();
}
addedPrinters.add(printer);
}
}
// Send the added printers, if such.
if (addedPrinters != null) {
sendAddedPrinters(mObserver, addedPrinters);
}
// Determine the removed printers.
List removedPrinterIds = null;
for (PrinterInfo sentPrinter : mLastSentPrinters.values()) {
if (!mPrinters.containsKey(sentPrinter.getId())) {
if (removedPrinterIds == null) {
removedPrinterIds = new ArrayList();
}
removedPrinterIds.add(sentPrinter.getId());
}
}
// Send the removed printers, if such.
if (removedPrinterIds != null) {
sendRemovedPrinters(mObserver, removedPrinterIds);
}
mLastSentPrinters = null;
}
/**
* Callback asking you to start printer discovery. Discovered printers should be
* added via calling {@link #addPrinters(List)}. Added printers that disappeared
* should be removed via calling {@link #removePrinters(List)}. Added printers
* whose properties or capabilities changed should be updated via calling {@link
* #addPrinters(List)}. You will receive a call to {@link #onStopPrinterDiscovery()}
* when you should stop printer discovery.
*
* During the lifetime of this session all printers that are known to your print
* service have to be added. The system does not retain any printers across sessions.
* However, if you were asked to start and then stop performing printer discovery
* in this session, then a subsequent discovering should not re-discover already
* discovered printers. You can get the printers reported during this session by
* calling {@link #getPrinters()}.
*
*
* Note: You are also given a list of printers whose availability
* has to be checked first. For example, these printers could be the user's favorite
* ones, therefore they have to be verified first. You do not need
* to provide the capabilities of the printers, rather verify whether they exist
* similarly to {@link #onValidatePrinters(List)}.
*
*
* @param priorityList The list of printers to validate first. Never null.
*
* @see #onStopPrinterDiscovery()
* @see #addPrinters(List)
* @see #removePrinters(List)
* @see #isPrinterDiscoveryStarted()
*/
public abstract void onStartPrinterDiscovery(List priorityList);
/**
* Callback notifying you that you should stop printer discovery.
*
* @see #onStartPrinterDiscovery(List)
* @see #isPrinterDiscoveryStarted()
*/
public abstract void onStopPrinterDiscovery();
/**
* Callback asking you to validate that the given printers are valid, that
* is they exist. You are responsible for checking whether these printers
* exist and for the ones that do exist notify the system via calling
* {@link #addPrinters(List)}.
*
* Note: You are not required to provide
* the printer capabilities when updating the printers that do exist.
*
*
* @param printerIds The printers to validate.
*
* @see PrinterInfo.Builder#setCapabilities(PrinterCapabilitiesInfo)
* PrinterInfo.Builder.setCapabilities(PrinterCapabilitiesInfo)
*/
public abstract void onValidatePrinters(List printerIds);
/**
* Callback asking you to start tracking the state of a printer. Tracking
* the state means that you should do a best effort to observe the state
* of this printer and notify the system if that state changes via calling
* {@link #addPrinters(List)}.
*
* Note: A printer can be initially added without its
* capabilities to avoid polling printers that the user will not select.
* However, after this method is called you are expected to update the
* printer including its capabilities. Otherwise, the
* printer will be ignored.
*
*
* A scenario when you may be requested to track a printer's state is if
* the user selects that printer and the system has to present print
* options UI based on the printer's capabilities. In this case the user
* should be promptly informed if, for example, the printer becomes
* unavailable.
*
*
* @param printerId The printer to start tracking.
*
* @see #onStopPrinterStateTracking(PrinterId)
* @see PrinterInfo.Builder#setCapabilities(PrinterCapabilitiesInfo)
* PrinterInfo.Builder.setCapabilities(PrinterCapabilitiesInfo)
*/
public abstract void onStartPrinterStateTracking(PrinterId printerId);
/**
* Callback asking you to stop tracking the state of a printer. The passed
* in printer id is the one for which you received a call to {@link
* #onStartPrinterStateTracking(PrinterId)}.
*
* @param printerId The printer to stop tracking.
*
* @see #onStartPrinterStateTracking(PrinterId)
*/
public abstract void onStopPrinterStateTracking(PrinterId printerId);
/**
* Gets the printers that should be tracked. These are printers that are
* important to the user and for which you received a call to {@link
* #onStartPrinterStateTracking(PrinterId)} asking you to observer their
* state and reporting it to the system via {@link #addPrinters(List)}.
* You will receive a call to {@link #onStopPrinterStateTracking(PrinterId)}
* if you should stop tracking a printer.
*
* Note: Calls to this method after the session is
* destroyed, that is after the {@link #onDestroy()} callback, will be ignored.
*
*
* @return The printers.
*
* @see #onStartPrinterStateTracking(PrinterId)
* @see #onStopPrinterStateTracking(PrinterId)
* @see #isDestroyed()
*/
public final List getTrackedPrinters() {
PrintService.throwIfNotCalledOnMainThread();
if (mIsDestroyed) {
return Collections.emptyList();
}
return new ArrayList(mTrackedPrinters);
}
/**
* Notifies you that the session is destroyed. After this callback is invoked
* any calls to the methods of this class will be ignored, {@link #isDestroyed()}
* will return true and you will also no longer receive callbacks.
*
* @see #isDestroyed()
*/
public abstract void onDestroy();
/**
* Gets whether the session is destroyed.
*
* @return Whether the session is destroyed.
*
* @see #onDestroy()
*/
public final boolean isDestroyed() {
PrintService.throwIfNotCalledOnMainThread();
return mIsDestroyed;
}
/**
* Gets whether printer discovery is started.
*
* @return Whether printer discovery is destroyed.
*
* @see #onStartPrinterDiscovery(List)
* @see #onStopPrinterDiscovery()
*/
public final boolean isPrinterDiscoveryStarted() {
PrintService.throwIfNotCalledOnMainThread();
return mIsDiscoveryStarted;
}
void startPrinterDiscovery(List priorityList) {
if (!mIsDestroyed) {
mIsDiscoveryStarted = true;
sendOutOfDiscoveryPeriodPrinterChanges();
if (priorityList == null) {
priorityList = Collections.emptyList();
}
onStartPrinterDiscovery(priorityList);
}
}
void stopPrinterDiscovery() {
if (!mIsDestroyed) {
mIsDiscoveryStarted = false;
onStopPrinterDiscovery();
}
}
void validatePrinters(List printerIds) {
if (!mIsDestroyed && mObserver != null) {
onValidatePrinters(printerIds);
}
}
void startPrinterStateTracking(PrinterId printerId) {
if (!mIsDestroyed && mObserver != null
&& !mTrackedPrinters.contains(printerId)) {
mTrackedPrinters.add(printerId);
onStartPrinterStateTracking(printerId);
}
}
void stopPrinterStateTracking(PrinterId printerId) {
if (!mIsDestroyed && mObserver != null
&& mTrackedPrinters.remove(printerId)) {
onStopPrinterStateTracking(printerId);
}
}
void destroy() {
if (!mIsDestroyed) {
mIsDestroyed = true;
mIsDiscoveryStarted = false;
mPrinters.clear();
mLastSentPrinters = null;
mObserver = null;
onDestroy();
}
}
}