/* * 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.CertificateStore; 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; private static final String IPP_SCHEME = "ipp"; private static final String IPPS_SCHEME = "ipps"; /** 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_SECURITY = 4; private static final int STATE_CANCEL = 5; private static final int STATE_DONE = 6; private final BuiltInPrintService mPrintService; private final PrintJob mPrintJob; private final Backend mBackend; private int mState; private Consumer mCompleteConsumer; private Uri mPath; private DelayedAction mDiscoveryTimeout; private P2pPrinterConnection mConnection; private LocalPrinterCapabilities mCapabilities; private CertificateStore mCertificateStore; /** * Construct the object; use {@link #start(Consumer)} to begin job processing. */ LocalPrintJob(BuiltInPrintService printService, Backend backend, PrintJob printJob) { mPrintService = printService; mBackend = backend; mPrintJob = printJob; mCertificateStore = mPrintService.getCertificateStore(); 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 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); } /** * Restart the job if possible. */ void restart() { if (DEBUG) Log.d(TAG, "restart() " + mPrintJob + " in state " + mState); if (mState == STATE_SECURITY) { mCapabilities.certificate = mCertificateStore.get(mCapabilities.uuid); deliver(); } } void cancel() { if (DEBUG) Log.d(TAG, "cancel() " + mPrintJob + " in state " + mState); switch (mState) { case STATE_DISCOVERY: case STATE_CAPABILITIES: case STATE_SECURITY: // 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; // Upgrade to IPPS path if present for (Uri path : printer.paths) { if (IPPS_SCHEME.equals(path.getScheme())) { mPath = path; break; } } 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(); } mCapabilities = capabilities; deliver(); } } private void deliver() { // Upgrade to IPPS if necessary Uri newUri = Uri.parse(mCapabilities.path); if (IPPS_SCHEME.equals(newUri.getScheme()) && newUri.getPort() > 0 && IPP_SCHEME.equals(mPath.getScheme())) { mPath = mPath.buildUpon().scheme(IPPS_SCHEME).encodedAuthority(mPath.getHost() + ":" + newUri.getPort()).build(); } if (DEBUG) Log.d(TAG, "deliver() to " + mPath); if (mCapabilities.certificate != null && !IPPS_SCHEME.equals(mPath.getScheme())) { mState = STATE_SECURITY; mPrintJob.block(mPrintService.getString(R.string.printer_not_encrypted)); mPrintService.notifyCertificateChange(mCapabilities.name, mPrintJob.getInfo().getPrinterId(), mCapabilities.uuid, null); } else { mState = STATE_DELIVERING; mPrintJob.start(); mBackend.print(mPath, mPrintJob, mCapabilities, this::handleJobStatus); } } private void handleJobStatus(JobStatus jobStatus) { if (DEBUG) Log.d(TAG, "onJobStatus() " + jobStatus); byte[] certificate = jobStatus.getCertificate(); if (certificate != null && mCapabilities != null) { // If there is no certificate, record this one if (mCertificateStore.get(mCapabilities.uuid) == null) { if (DEBUG) Log.d(TAG, "Recording new certificate"); mCertificateStore.put(mCapabilities.uuid, certificate); } } 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 if (jobStatus.getBlockedReasonId() == R.string.printer_bad_certificate) { handleBadCertificate(jobStatus); } else { 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; } } private void handleBadCertificate(JobStatus jobStatus) { byte[] certificate = jobStatus.getCertificate(); if (certificate == null) { mPrintJob.fail(mPrintService.getString(R.string.printer_bad_certificate)); } else { if (DEBUG) Log.d(TAG, "Certificate change detected."); mState = STATE_SECURITY; mPrintJob.block(mPrintService.getString(R.string.printer_bad_certificate)); mPrintService.notifyCertificateChange(mCapabilities.name, mPrintJob.getInfo().getPrinterId(), mCapabilities.uuid, certificate); } } /** * 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); } }