blob: ba0776ce9fd0eaa7a9da2b5f2f7f8f51f83f6719 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
* Copyright (C) 2016 Mopria Alliance, Inc.
*
* 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 com.android.bips;
import android.net.Uri;
import android.print.PrintJobId;
import android.printservice.PrintJob;
import android.util.Log;
import com.android.bips.discovery.ConnectionListener;
import com.android.bips.discovery.DiscoveredPrinter;
import com.android.bips.discovery.MdnsDiscovery;
import com.android.bips.ipp.Backend;
import com.android.bips.ipp.CapabilitiesCache;
import com.android.bips.ipp.JobStatus;
import com.android.bips.jni.BackendConstants;
import com.android.bips.jni.LocalPrinterCapabilities;
import com.android.bips.p2p.P2pPrinterConnection;
import com.android.bips.p2p.P2pUtils;
import java.util.function.Consumer;
/**
* Manage the process of delivering a print job
*/
class LocalPrintJob implements MdnsDiscovery.Listener, ConnectionListener,
CapabilitiesCache.OnLocalPrinterCapabilities {
private static final String TAG = LocalPrintJob.class.getSimpleName();
private static final boolean DEBUG = false;
/** Maximum time to wait to find a printer before failing the job */
private static final int DISCOVERY_TIMEOUT = 2 * 60 * 1000;
// Internal job states
private static final int STATE_INIT = 0;
private static final int STATE_DISCOVERY = 1;
private static final int STATE_CAPABILITIES = 2;
private static final int STATE_DELIVERING = 3;
private static final int STATE_CANCEL = 4;
private static final int STATE_DONE = 5;
private final BuiltInPrintService mPrintService;
private final PrintJob mPrintJob;
private final Backend mBackend;
private int mState;
private Consumer<LocalPrintJob> mCompleteConsumer;
private Uri mPath;
private DelayedAction mDiscoveryTimeout;
private P2pPrinterConnection mConnection;
/**
* Construct the object; use {@link #start(Consumer)} to begin job processing.
*/
LocalPrintJob(BuiltInPrintService printService, Backend backend, PrintJob printJob) {
mPrintService = printService;
mBackend = backend;
mPrintJob = printJob;
mState = STATE_INIT;
// Tell the job it is blocked (until start())
mPrintJob.start();
mPrintJob.block(printService.getString(R.string.waiting_to_send));
}
/**
* Begin the process of delivering the job. Internally, discovers the target printer,
* obtains its capabilities, delivers the job to the printer, and waits for job completion.
*
* @param callback Callback to be issued when job processing is complete
*/
void start(Consumer<LocalPrintJob> callback) {
if (DEBUG) Log.d(TAG, "start() " + mPrintJob);
if (mState != STATE_INIT) {
Log.w(TAG, "Invalid start state " + mState);
return;
}
mPrintJob.start();
// Acquire a lock so that WiFi isn't put to sleep while we send the job
mPrintService.lockWifi();
mState = STATE_DISCOVERY;
mCompleteConsumer = callback;
mDiscoveryTimeout = mPrintService.delay(DISCOVERY_TIMEOUT, () -> {
if (DEBUG) Log.d(TAG, "Discovery timeout");
if (mState == STATE_DISCOVERY) {
finish(false, mPrintService.getString(R.string.printer_offline));
}
});
mPrintService.getDiscovery().start(this);
}
void cancel() {
if (DEBUG) Log.d(TAG, "cancel() " + mPrintJob + " in state " + mState);
switch (mState) {
case STATE_DISCOVERY:
case STATE_CAPABILITIES:
// Cancel immediately
mState = STATE_CANCEL;
finish(false, null);
break;
case STATE_DELIVERING:
// Request cancel and wait for completion
mState = STATE_CANCEL;
mBackend.cancel();
break;
}
}
PrintJobId getPrintJobId() {
return mPrintJob.getId();
}
@Override
public void onPrinterFound(DiscoveredPrinter printer) {
if (mState != STATE_DISCOVERY) {
return;
}
if (!printer.getId(mPrintService).equals(mPrintJob.getInfo().getPrinterId())) {
return;
}
if (DEBUG) Log.d(TAG, "onPrinterFound() " + printer.name + " state=" + mState);
if (P2pUtils.isP2p(printer)) {
// Launch a P2P connection attempt
mConnection = new P2pPrinterConnection(mPrintService, printer, this);
return;
}
if (P2pUtils.isOnConnectedInterface(mPrintService, printer) && mConnection == null) {
// Hold the P2P connection up during printing
mConnection = new P2pPrinterConnection(mPrintService, printer, this);
}
// We have a good path so stop discovering and get capabilities
mPrintService.getDiscovery().stop(this);
mState = STATE_CAPABILITIES;
mPath = printer.path;
mPrintService.getCapabilitiesCache().request(printer, true, this);
}
@Override
public void onPrinterLost(DiscoveredPrinter printer) {
// Ignore (the capability request, if any, will fail)
}
@Override
public void onConnectionComplete(DiscoveredPrinter printer) {
// Ignore late connection events
if (mState != STATE_DISCOVERY) {
return;
}
if (printer == null) {
finish(false, mPrintService.getString(R.string.failed_printer_connection));
} else if (mPrintJob.isBlocked()) {
mPrintJob.start();
}
}
@Override
public void onConnectionDelayed(boolean delayed) {
if (DEBUG) Log.d(TAG, "onConnectionDelayed " + delayed);
// Ignore late events
if (mState != STATE_DISCOVERY) {
return;
}
if (delayed) {
mPrintJob.block(mPrintService.getString(R.string.connect_hint_text));
} else {
// Remove block message
mPrintJob.start();
}
}
PrintJob getPrintJob() {
return mPrintJob;
}
@Override
public void onCapabilities(LocalPrinterCapabilities capabilities) {
if (DEBUG) Log.d(TAG, "Capabilities for " + mPath + " are " + capabilities);
if (mState != STATE_CAPABILITIES) {
return;
}
if (capabilities == null) {
finish(false, mPrintService.getString(R.string.printer_offline));
} else {
if (DEBUG) Log.d(TAG, "Starting backend print of " + mPrintJob);
if (mDiscoveryTimeout != null) {
mDiscoveryTimeout.cancel();
}
mState = STATE_DELIVERING;
mPrintJob.start();
mBackend.print(mPath, mPrintJob, capabilities, this::handleJobStatus);
}
}
private void handleJobStatus(JobStatus jobStatus) {
if (DEBUG) Log.d(TAG, "onJobStatus() " + jobStatus);
switch (jobStatus.getJobState()) {
case BackendConstants.JOB_STATE_DONE:
switch (jobStatus.getJobResult()) {
case BackendConstants.JOB_DONE_OK:
finish(true, null);
break;
case BackendConstants.JOB_DONE_CANCELLED:
mState = STATE_CANCEL;
finish(false, null);
break;
case BackendConstants.JOB_DONE_CORRUPT:
finish(false, mPrintService.getString(R.string.unreadable_input));
break;
default:
// Job failed
finish(false, null);
break;
}
break;
case BackendConstants.JOB_STATE_BLOCKED:
if (mState == STATE_CANCEL) {
return;
}
int blockedId = jobStatus.getBlockedReasonId();
blockedId = (blockedId == 0) ? R.string.printer_check : blockedId;
String blockedReason = mPrintService.getString(blockedId);
mPrintJob.block(blockedReason);
break;
case BackendConstants.JOB_STATE_RUNNING:
if (mState == STATE_CANCEL) {
return;
}
mPrintJob.start();
break;
}
}
/**
* Terminate the job, issuing appropriate notifications.
*
* @param success true if the printer reported successful job completion
* @param error reason for job failure if known
*/
private void finish(boolean success, String error) {
if (DEBUG) Log.d(TAG, "finish() success=" + success + ", error=" + error);
mPrintService.getDiscovery().stop(this);
if (mDiscoveryTimeout != null) {
mDiscoveryTimeout.cancel();
}
if (mConnection != null) {
mConnection.close();
}
mPrintService.unlockWifi();
mBackend.closeDocument();
if (success) {
// Job must not be blocked before completion
mPrintJob.start();
mPrintJob.complete();
} else if (mState == STATE_CANCEL) {
mPrintJob.cancel();
} else {
mPrintJob.fail(error);
}
mState = STATE_DONE;
mCompleteConsumer.accept(LocalPrintJob.this);
}
}