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