1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * Copyright (C) 2016 Mopria Alliance, Inc. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.bips; 19 20 import android.net.Uri; 21 import android.print.PrintJobId; 22 import android.printservice.PrintJob; 23 import android.util.Log; 24 25 import com.android.bips.discovery.ConnectionListener; 26 import com.android.bips.discovery.DiscoveredPrinter; 27 import com.android.bips.discovery.MdnsDiscovery; 28 import com.android.bips.ipp.Backend; 29 import com.android.bips.ipp.CapabilitiesCache; 30 import com.android.bips.ipp.CertificateStore; 31 import com.android.bips.ipp.JobStatus; 32 import com.android.bips.jni.BackendConstants; 33 import com.android.bips.jni.LocalPrinterCapabilities; 34 import com.android.bips.p2p.P2pPrinterConnection; 35 import com.android.bips.p2p.P2pUtils; 36 37 import java.util.function.Consumer; 38 39 /** 40 * Manage the process of delivering a print job 41 */ 42 class LocalPrintJob implements MdnsDiscovery.Listener, ConnectionListener, 43 CapabilitiesCache.OnLocalPrinterCapabilities { 44 private static final String TAG = LocalPrintJob.class.getSimpleName(); 45 private static final boolean DEBUG = false; 46 private static final String IPPS_SCHEME = "ipps"; 47 48 /** Maximum time to wait to find a printer before failing the job */ 49 private static final int DISCOVERY_TIMEOUT = 2 * 60 * 1000; 50 51 // Internal job states 52 private static final int STATE_INIT = 0; 53 private static final int STATE_DISCOVERY = 1; 54 private static final int STATE_CAPABILITIES = 2; 55 private static final int STATE_DELIVERING = 3; 56 private static final int STATE_SECURITY = 4; 57 private static final int STATE_CANCEL = 5; 58 private static final int STATE_DONE = 6; 59 60 private final BuiltInPrintService mPrintService; 61 private final PrintJob mPrintJob; 62 private final Backend mBackend; 63 64 private int mState; 65 private Consumer<LocalPrintJob> mCompleteConsumer; 66 private Uri mPath; 67 private DelayedAction mDiscoveryTimeout; 68 private P2pPrinterConnection mConnection; 69 private LocalPrinterCapabilities mCapabilities; 70 private CertificateStore mCertificateStore; 71 72 /** 73 * Construct the object; use {@link #start(Consumer)} to begin job processing. 74 */ LocalPrintJob(BuiltInPrintService printService, Backend backend, PrintJob printJob)75 LocalPrintJob(BuiltInPrintService printService, Backend backend, PrintJob printJob) { 76 mPrintService = printService; 77 mBackend = backend; 78 mPrintJob = printJob; 79 mCertificateStore = mPrintService.getCertificateStore(); 80 mState = STATE_INIT; 81 82 // Tell the job it is blocked (until start()) 83 mPrintJob.start(); 84 mPrintJob.block(printService.getString(R.string.waiting_to_send)); 85 } 86 87 /** 88 * Begin the process of delivering the job. Internally, discovers the target printer, 89 * obtains its capabilities, delivers the job to the printer, and waits for job completion. 90 * 91 * @param callback Callback to be issued when job processing is complete 92 */ start(Consumer<LocalPrintJob> callback)93 void start(Consumer<LocalPrintJob> callback) { 94 if (DEBUG) Log.d(TAG, "start() " + mPrintJob); 95 if (mState != STATE_INIT) { 96 Log.w(TAG, "Invalid start state " + mState); 97 return; 98 } 99 mPrintJob.start(); 100 101 // Acquire a lock so that WiFi isn't put to sleep while we send the job 102 mPrintService.lockWifi(); 103 104 mState = STATE_DISCOVERY; 105 mCompleteConsumer = callback; 106 mDiscoveryTimeout = mPrintService.delay(DISCOVERY_TIMEOUT, () -> { 107 if (DEBUG) Log.d(TAG, "Discovery timeout"); 108 if (mState == STATE_DISCOVERY) { 109 finish(false, mPrintService.getString(R.string.printer_offline)); 110 } 111 }); 112 113 mPrintService.getDiscovery().start(this); 114 } 115 116 /** 117 * Restart the job if possible. 118 */ restart()119 void restart() { 120 if (DEBUG) Log.d(TAG, "restart() " + mPrintJob + " in state " + mState); 121 if (mState == STATE_SECURITY) { 122 mCapabilities.certificate = mCertificateStore.get(mCapabilities.uuid); 123 deliver(); 124 } 125 } 126 cancel()127 void cancel() { 128 if (DEBUG) Log.d(TAG, "cancel() " + mPrintJob + " in state " + mState); 129 130 switch (mState) { 131 case STATE_DISCOVERY: 132 case STATE_CAPABILITIES: 133 case STATE_SECURITY: 134 // Cancel immediately 135 mState = STATE_CANCEL; 136 finish(false, null); 137 break; 138 139 case STATE_DELIVERING: 140 // Request cancel and wait for completion 141 mState = STATE_CANCEL; 142 mBackend.cancel(); 143 break; 144 } 145 } 146 getPrintJobId()147 PrintJobId getPrintJobId() { 148 return mPrintJob.getId(); 149 } 150 151 @Override onPrinterFound(DiscoveredPrinter printer)152 public void onPrinterFound(DiscoveredPrinter printer) { 153 if (mState != STATE_DISCOVERY) { 154 return; 155 } 156 if (!printer.getId(mPrintService).equals(mPrintJob.getInfo().getPrinterId())) { 157 return; 158 } 159 160 if (DEBUG) Log.d(TAG, "onPrinterFound() " + printer.name + " state=" + mState); 161 162 if (P2pUtils.isP2p(printer)) { 163 // Launch a P2P connection attempt 164 mConnection = new P2pPrinterConnection(mPrintService, printer, this); 165 return; 166 } 167 168 if (P2pUtils.isOnConnectedInterface(mPrintService, printer) && mConnection == null) { 169 // Hold the P2P connection up during printing 170 mConnection = new P2pPrinterConnection(mPrintService, printer, this); 171 } 172 173 // We have a good path so stop discovering and get capabilities 174 mPrintService.getDiscovery().stop(this); 175 mState = STATE_CAPABILITIES; 176 mPath = printer.path; 177 // Upgrade to IPPS path if present 178 for (Uri path : printer.paths) { 179 if (IPPS_SCHEME.equals(path.getScheme())) { 180 mPath = path; 181 break; 182 } 183 } 184 185 mPrintService.getCapabilitiesCache().request(printer, true, this); 186 } 187 188 @Override onPrinterLost(DiscoveredPrinter printer)189 public void onPrinterLost(DiscoveredPrinter printer) { 190 // Ignore (the capability request, if any, will fail) 191 } 192 193 @Override onConnectionComplete(DiscoveredPrinter printer)194 public void onConnectionComplete(DiscoveredPrinter printer) { 195 // Ignore late connection events 196 if (mState != STATE_DISCOVERY) { 197 return; 198 } 199 200 if (printer == null) { 201 finish(false, mPrintService.getString(R.string.failed_printer_connection)); 202 } else if (mPrintJob.isBlocked()) { 203 mPrintJob.start(); 204 } 205 } 206 207 @Override onConnectionDelayed(boolean delayed)208 public void onConnectionDelayed(boolean delayed) { 209 if (DEBUG) Log.d(TAG, "onConnectionDelayed " + delayed); 210 211 // Ignore late events 212 if (mState != STATE_DISCOVERY) { 213 return; 214 } 215 216 if (delayed) { 217 mPrintJob.block(mPrintService.getString(R.string.connect_hint_text)); 218 } else { 219 // Remove block message 220 mPrintJob.start(); 221 } 222 } 223 getPrintJob()224 PrintJob getPrintJob() { 225 return mPrintJob; 226 } 227 228 @Override onCapabilities(LocalPrinterCapabilities capabilities)229 public void onCapabilities(LocalPrinterCapabilities capabilities) { 230 if (DEBUG) Log.d(TAG, "Capabilities for " + mPath + " are " + capabilities); 231 if (mState != STATE_CAPABILITIES) { 232 return; 233 } 234 235 if (capabilities == null) { 236 finish(false, mPrintService.getString(R.string.printer_offline)); 237 } else { 238 if (DEBUG) Log.d(TAG, "Starting backend print of " + mPrintJob); 239 if (mDiscoveryTimeout != null) { 240 mDiscoveryTimeout.cancel(); 241 } 242 mCapabilities = capabilities; 243 deliver(); 244 } 245 } 246 deliver()247 private void deliver() { 248 if (mCapabilities.certificate != null && !IPPS_SCHEME.equals(mPath.getScheme())) { 249 mState = STATE_SECURITY; 250 mPrintJob.block(mPrintService.getString(R.string.printer_not_encrypted)); 251 mPrintService.notifyCertificateChange(mCapabilities.name, 252 mPrintJob.getInfo().getPrinterId(), mCapabilities.uuid, null); 253 } else { 254 mState = STATE_DELIVERING; 255 mPrintJob.start(); 256 mBackend.print(mPath, mPrintJob, mCapabilities, this::handleJobStatus); 257 } 258 } 259 handleJobStatus(JobStatus jobStatus)260 private void handleJobStatus(JobStatus jobStatus) { 261 if (DEBUG) Log.d(TAG, "onJobStatus() " + jobStatus); 262 263 byte[] certificate = jobStatus.getCertificate(); 264 if (certificate != null && mCapabilities != null) { 265 // If there is no certificate, record this one 266 if (mCertificateStore.get(mCapabilities.uuid) == null) { 267 if (DEBUG) Log.d(TAG, "Recording new certificate"); 268 mCertificateStore.put(mCapabilities.uuid, certificate); 269 } 270 } 271 272 switch (jobStatus.getJobState()) { 273 case BackendConstants.JOB_STATE_DONE: 274 switch (jobStatus.getJobResult()) { 275 case BackendConstants.JOB_DONE_OK: 276 finish(true, null); 277 break; 278 case BackendConstants.JOB_DONE_CANCELLED: 279 mState = STATE_CANCEL; 280 finish(false, null); 281 break; 282 case BackendConstants.JOB_DONE_CORRUPT: 283 finish(false, mPrintService.getString(R.string.unreadable_input)); 284 break; 285 default: 286 // Job failed 287 if (jobStatus.getBlockedReasonId() == R.string.printer_bad_certificate) { 288 handleBadCertificate(jobStatus); 289 } else { 290 finish(false, null); 291 } 292 break; 293 } 294 break; 295 296 case BackendConstants.JOB_STATE_BLOCKED: 297 if (mState == STATE_CANCEL) { 298 return; 299 } 300 int blockedId = jobStatus.getBlockedReasonId(); 301 blockedId = (blockedId == 0) ? R.string.printer_check : blockedId; 302 String blockedReason = mPrintService.getString(blockedId); 303 mPrintJob.block(blockedReason); 304 break; 305 306 case BackendConstants.JOB_STATE_RUNNING: 307 if (mState == STATE_CANCEL) { 308 return; 309 } 310 mPrintJob.start(); 311 break; 312 } 313 } 314 handleBadCertificate(JobStatus jobStatus)315 private void handleBadCertificate(JobStatus jobStatus) { 316 byte[] certificate = jobStatus.getCertificate(); 317 318 if (certificate == null) { 319 mPrintJob.fail(mPrintService.getString(R.string.printer_bad_certificate)); 320 } else { 321 if (DEBUG) Log.d(TAG, "Certificate change detected."); 322 mState = STATE_SECURITY; 323 mPrintJob.block(mPrintService.getString(R.string.printer_bad_certificate)); 324 mPrintService.notifyCertificateChange(mCapabilities.name, 325 mPrintJob.getInfo().getPrinterId(), mCapabilities.uuid, certificate); 326 } 327 } 328 329 /** 330 * Terminate the job, issuing appropriate notifications. 331 * 332 * @param success true if the printer reported successful job completion 333 * @param error reason for job failure if known 334 */ finish(boolean success, String error)335 private void finish(boolean success, String error) { 336 if (DEBUG) Log.d(TAG, "finish() success=" + success + ", error=" + error); 337 mPrintService.getDiscovery().stop(this); 338 if (mDiscoveryTimeout != null) { 339 mDiscoveryTimeout.cancel(); 340 } 341 if (mConnection != null) { 342 mConnection.close(); 343 } 344 mPrintService.unlockWifi(); 345 mBackend.closeDocument(); 346 if (success) { 347 // Job must not be blocked before completion 348 mPrintJob.start(); 349 mPrintJob.complete(); 350 } else if (mState == STATE_CANCEL) { 351 mPrintJob.cancel(); 352 } else { 353 mPrintJob.fail(error); 354 } 355 mState = STATE_DONE; 356 mCompleteConsumer.accept(LocalPrintJob.this); 357 } 358 } 359