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.os.Bundle; 22 import android.print.PrintJobId; 23 import android.printservice.PrintJob; 24 import android.util.Log; 25 26 import com.android.bips.discovery.ConnectionListener; 27 import com.android.bips.discovery.DiscoveredPrinter; 28 import com.android.bips.discovery.MdnsDiscovery; 29 import com.android.bips.ipp.Backend; 30 import com.android.bips.ipp.CapabilitiesCache; 31 import com.android.bips.ipp.CertificateStore; 32 import com.android.bips.ipp.JobStatus; 33 import com.android.bips.jni.BackendConstants; 34 import com.android.bips.jni.LocalPrinterCapabilities; 35 import com.android.bips.p2p.P2pPrinterConnection; 36 import com.android.bips.p2p.P2pUtils; 37 38 import java.util.ArrayList; 39 import java.util.StringJoiner; 40 import java.util.function.Consumer; 41 42 /** 43 * Manage the process of delivering a print job 44 */ 45 class LocalPrintJob implements MdnsDiscovery.Listener, ConnectionListener, 46 CapabilitiesCache.OnLocalPrinterCapabilities { 47 private static final String TAG = LocalPrintJob.class.getSimpleName(); 48 private static final boolean DEBUG = false; 49 private static final String IPP_SCHEME = "ipp"; 50 private static final String IPPS_SCHEME = "ipps"; 51 52 /** Maximum time to wait to find a printer before failing the job */ 53 private static final int DISCOVERY_TIMEOUT = 2 * 60 * 1000; 54 55 // Internal job states 56 private static final int STATE_INIT = 0; 57 private static final int STATE_DISCOVERY = 1; 58 private static final int STATE_CAPABILITIES = 2; 59 private static final int STATE_DELIVERING = 3; 60 private static final int STATE_SECURITY = 4; 61 private static final int STATE_CANCEL = 5; 62 private static final int STATE_DONE = 6; 63 64 private final BuiltInPrintService mPrintService; 65 private final PrintJob mPrintJob; 66 private final Backend mBackend; 67 68 private int mState; 69 private Consumer<LocalPrintJob> mCompleteConsumer; 70 private Uri mPath; 71 private DelayedAction mDiscoveryTimeout; 72 private P2pPrinterConnection mConnection; 73 private LocalPrinterCapabilities mCapabilities; 74 private CertificateStore mCertificateStore; 75 private long mStartTime; 76 private ArrayList<String> mBlockedReasons = new ArrayList<>(); 77 78 /** 79 * Construct the object; use {@link #start(Consumer)} to begin job processing. 80 */ LocalPrintJob(BuiltInPrintService printService, Backend backend, PrintJob printJob)81 LocalPrintJob(BuiltInPrintService printService, Backend backend, PrintJob printJob) { 82 mPrintService = printService; 83 mBackend = backend; 84 mPrintJob = printJob; 85 mCertificateStore = mPrintService.getCertificateStore(); 86 mState = STATE_INIT; 87 88 // Tell the job it is blocked (until start()) 89 mPrintJob.start(); 90 mPrintJob.block(printService.getString(R.string.waiting_to_send)); 91 } 92 93 /** 94 * Begin the process of delivering the job. Internally, discovers the target printer, 95 * obtains its capabilities, delivers the job to the printer, and waits for job completion. 96 * 97 * @param callback Callback to be issued when job processing is complete 98 */ start(Consumer<LocalPrintJob> callback)99 void start(Consumer<LocalPrintJob> callback) { 100 mStartTime = System.currentTimeMillis(); 101 // TODO: Log job attempted event here using getJobAttemptedBundle() 102 if (DEBUG) Log.d(TAG, "start() " + mPrintJob); 103 if (mState != STATE_INIT) { 104 Log.w(TAG, "Invalid start state " + mState); 105 return; 106 } 107 mPrintJob.start(); 108 109 // Acquire a lock so that WiFi isn't put to sleep while we send the job 110 mPrintService.lockWifi(); 111 112 mState = STATE_DISCOVERY; 113 mCompleteConsumer = callback; 114 mDiscoveryTimeout = mPrintService.delay(DISCOVERY_TIMEOUT, () -> { 115 if (DEBUG) Log.d(TAG, "Discovery timeout"); 116 if (mState == STATE_DISCOVERY) { 117 finish(false, mPrintService.getString(R.string.printer_offline)); 118 } 119 }); 120 121 mPrintService.getDiscovery().start(this); 122 } 123 124 /** 125 * Restart the job if possible. 126 */ restart()127 void restart() { 128 if (DEBUG) Log.d(TAG, "restart() " + mPrintJob + " in state " + mState); 129 if (mState == STATE_SECURITY) { 130 mCapabilities.certificate = mCertificateStore.get(mCapabilities.uuid); 131 deliver(); 132 } 133 } 134 cancel()135 void cancel() { 136 if (DEBUG) Log.d(TAG, "cancel() " + mPrintJob + " in state " + mState); 137 138 switch (mState) { 139 case STATE_DISCOVERY: 140 case STATE_CAPABILITIES: 141 case STATE_SECURITY: 142 // Cancel immediately 143 mState = STATE_CANCEL; 144 finish(false, null); 145 break; 146 147 case STATE_DELIVERING: 148 // Request cancel and wait for completion 149 mState = STATE_CANCEL; 150 mBackend.cancel(); 151 break; 152 } 153 Bundle bundle = getJobCompletedAnalyticsBundle(BackendConstants.JOB_DONE_CANCELLED); 154 bundle.putString(BackendConstants.PARAM_ERROR_MESSAGES, getStringifiedBlockedReasons()); 155 // TODO: Log job completed event here with the above bundle 156 } 157 getPrintJobId()158 PrintJobId getPrintJobId() { 159 return mPrintJob.getId(); 160 } 161 162 @Override onPrinterFound(DiscoveredPrinter printer)163 public void onPrinterFound(DiscoveredPrinter printer) { 164 if (mState != STATE_DISCOVERY) { 165 return; 166 } 167 if (!printer.getId(mPrintService).equals(mPrintJob.getInfo().getPrinterId())) { 168 return; 169 } 170 171 if (DEBUG) Log.d(TAG, "onPrinterFound() " + printer.name + " state=" + mState); 172 173 if (P2pUtils.isP2p(printer)) { 174 // Launch a P2P connection attempt 175 mConnection = new P2pPrinterConnection(mPrintService, printer, this); 176 return; 177 } 178 179 if (P2pUtils.isOnConnectedInterface(mPrintService, printer) && mConnection == null) { 180 // Hold the P2P connection up during printing 181 mConnection = new P2pPrinterConnection(mPrintService, printer, this); 182 } 183 184 // We have a good path so stop discovering and get capabilities 185 mPrintService.getDiscovery().stop(this); 186 mState = STATE_CAPABILITIES; 187 mPath = printer.path; 188 // Upgrade to IPPS path if present 189 for (Uri path : printer.paths) { 190 if (IPPS_SCHEME.equals(path.getScheme())) { 191 mPath = path; 192 break; 193 } 194 } 195 196 mPrintService.getCapabilitiesCache().request(printer, true, this); 197 } 198 199 @Override onPrinterLost(DiscoveredPrinter printer)200 public void onPrinterLost(DiscoveredPrinter printer) { 201 // Ignore (the capability request, if any, will fail) 202 } 203 204 @Override onConnectionComplete(DiscoveredPrinter printer)205 public void onConnectionComplete(DiscoveredPrinter printer) { 206 // Ignore late connection events 207 if (mState != STATE_DISCOVERY) { 208 return; 209 } 210 211 if (printer == null) { 212 finish(false, mPrintService.getString(R.string.failed_printer_connection)); 213 } else if (mPrintJob.isBlocked()) { 214 mPrintJob.start(); 215 } 216 } 217 218 @Override onConnectionDelayed(boolean delayed)219 public void onConnectionDelayed(boolean delayed) { 220 if (DEBUG) Log.d(TAG, "onConnectionDelayed " + delayed); 221 222 // Ignore late events 223 if (mState != STATE_DISCOVERY) { 224 return; 225 } 226 227 if (delayed) { 228 mPrintJob.block(mPrintService.getString(R.string.connect_hint_text)); 229 } else { 230 // Remove block message 231 mPrintJob.start(); 232 } 233 } 234 getPrintJob()235 PrintJob getPrintJob() { 236 return mPrintJob; 237 } 238 239 @Override onCapabilities(LocalPrinterCapabilities capabilities)240 public void onCapabilities(LocalPrinterCapabilities capabilities) { 241 if (DEBUG) Log.d(TAG, "Capabilities for " + mPath + " are " + capabilities); 242 if (mState != STATE_CAPABILITIES) { 243 return; 244 } 245 246 if (capabilities == null) { 247 finish(false, mPrintService.getString(R.string.printer_offline)); 248 } else { 249 if (DEBUG) Log.d(TAG, "Starting backend print of " + mPrintJob); 250 if (mDiscoveryTimeout != null) { 251 mDiscoveryTimeout.cancel(); 252 } 253 mCapabilities = capabilities; 254 deliver(); 255 } 256 } 257 deliver()258 private void deliver() { 259 // Upgrade to IPPS if necessary 260 Uri newUri = Uri.parse(mCapabilities.path); 261 if (IPPS_SCHEME.equals(newUri.getScheme()) && newUri.getPort() > 0 && 262 IPP_SCHEME.equals(mPath.getScheme())) { 263 mPath = mPath.buildUpon().scheme(IPPS_SCHEME).encodedAuthority(mPath.getHost() + 264 ":" + newUri.getPort()).build(); 265 } 266 267 if (DEBUG) Log.d(TAG, "deliver() to " + mPath); 268 if (mCapabilities.certificate != null && !IPPS_SCHEME.equals(mPath.getScheme())) { 269 mState = STATE_SECURITY; 270 mPrintJob.block(mPrintService.getString(R.string.printer_not_encrypted)); 271 mPrintService.notifyCertificateChange(mCapabilities.name, 272 mPrintJob.getInfo().getPrinterId(), mCapabilities.uuid, null); 273 } else { 274 mState = STATE_DELIVERING; 275 mPrintJob.start(); 276 mBackend.print(mPath, mPrintJob, mCapabilities, this::handleJobStatus); 277 } 278 } 279 handleJobStatus(JobStatus jobStatus)280 private void handleJobStatus(JobStatus jobStatus) { 281 if (DEBUG) Log.d(TAG, "onJobStatus() " + jobStatus); 282 283 byte[] certificate = jobStatus.getCertificate(); 284 if (certificate != null && mCapabilities != null) { 285 // If there is no certificate, record this one 286 if (mCertificateStore.get(mCapabilities.uuid) == null) { 287 if (DEBUG) Log.d(TAG, "Recording new certificate"); 288 mCertificateStore.put(mCapabilities.uuid, certificate); 289 } 290 } 291 292 mBlockedReasons.addAll(jobStatus.getBlockedReasons()); 293 294 switch (jobStatus.getJobState()) { 295 case BackendConstants.JOB_STATE_DONE: 296 Bundle bundle = getJobCompletedAnalyticsBundle(jobStatus.getJobResult()); 297 298 switch (jobStatus.getJobResult()) { 299 case BackendConstants.JOB_DONE_OK: 300 finish(true, null); 301 break; 302 case BackendConstants.JOB_DONE_CANCELLED: 303 mState = STATE_CANCEL; 304 finish(false, null); 305 bundle.putString( 306 BackendConstants.PARAM_ERROR_MESSAGES, 307 getStringifiedBlockedReasons()); 308 break; 309 case BackendConstants.JOB_DONE_CORRUPT: 310 finish(false, mPrintService.getString(R.string.unreadable_input)); 311 bundle.putString( 312 BackendConstants.PARAM_ERROR_MESSAGES, 313 getStringifiedBlockedReasons()); 314 break; 315 case BackendConstants.JOB_DONE_BAD_CERTIFICATE: 316 handleBadCertificate(jobStatus); 317 break; 318 default: 319 // Job failed 320 finish(false, null); 321 bundle.putString( 322 BackendConstants.PARAM_ERROR_MESSAGES, 323 getStringifiedBlockedReasons()); 324 break; 325 } 326 // TODO: Log JobCompleted analytic with the bundle here 327 break; 328 329 case BackendConstants.JOB_STATE_BLOCKED: 330 if (mState == STATE_CANCEL) { 331 return; 332 } 333 int blockedId = jobStatus.getBlockedReasonId(); 334 blockedId = (blockedId == 0) ? R.string.printer_check : blockedId; 335 String blockedReason = mPrintService.getString(blockedId); 336 mPrintJob.block(blockedReason); 337 break; 338 339 case BackendConstants.JOB_STATE_RUNNING: 340 if (mState == STATE_CANCEL) { 341 return; 342 } 343 mPrintJob.start(); 344 break; 345 } 346 } 347 handleBadCertificate(JobStatus jobStatus)348 private void handleBadCertificate(JobStatus jobStatus) { 349 byte[] certificate = jobStatus.getCertificate(); 350 351 if (certificate == null) { 352 mPrintJob.fail(mPrintService.getString(R.string.printer_bad_certificate)); 353 } else { 354 if (DEBUG) Log.d(TAG, "Certificate change detected."); 355 mState = STATE_SECURITY; 356 mPrintJob.block(mPrintService.getString(R.string.printer_bad_certificate)); 357 mPrintService.notifyCertificateChange(mCapabilities.name, 358 mPrintJob.getInfo().getPrinterId(), mCapabilities.uuid, certificate); 359 } 360 } 361 362 /** 363 * Terminate the job, issuing appropriate notifications. 364 * 365 * @param success true if the printer reported successful job completion 366 * @param error reason for job failure if known 367 */ finish(boolean success, String error)368 private void finish(boolean success, String error) { 369 if (DEBUG) Log.d(TAG, "finish() success=" + success + ", error=" + error); 370 mPrintService.getDiscovery().stop(this); 371 if (mDiscoveryTimeout != null) { 372 mDiscoveryTimeout.cancel(); 373 } 374 if (mConnection != null) { 375 mConnection.close(); 376 } 377 mPrintService.unlockWifi(); 378 mBackend.closeDocument(); 379 if (success) { 380 // Job must not be blocked before completion 381 mPrintJob.start(); 382 mPrintJob.complete(); 383 } else if (mState == STATE_CANCEL) { 384 mPrintJob.cancel(); 385 } else { 386 mPrintJob.fail(error); 387 } 388 mState = STATE_DONE; 389 mCompleteConsumer.accept(LocalPrintJob.this); 390 } 391 392 /** 393 * Get stringified blocked reasons delimited by '|' 394 * @return delimited string of blocked reasons 395 */ getStringifiedBlockedReasons()396 private String getStringifiedBlockedReasons() { 397 StringJoiner reasons = new StringJoiner("|"); 398 for (String reason: mBlockedReasons) { 399 reasons.add(reason); 400 } 401 return reasons.toString(); 402 } 403 404 /** 405 * Get the job completed analytics bundle 406 * 407 * @param result result of the job 408 * @return analytics bundle 409 */ getJobCompletedAnalyticsBundle(String result)410 private Bundle getJobCompletedAnalyticsBundle(String result) { 411 Bundle bundle = new Bundle(); 412 bundle.putString(BackendConstants.PARAM_JOB_ID, mPrintJob.getId().toString()); 413 bundle.putLong(BackendConstants.PARAM_DATE_TIME, System.currentTimeMillis()); 414 // TODO: Add real location 415 bundle.putString(BackendConstants.PARAM_LOCATION, "United States"); 416 // TODO: Add real user id 417 bundle.putString(BackendConstants.PARAM_USER_ID, "userid"); 418 bundle.putString(BackendConstants.PARAM_RESULT, result); 419 bundle.putLong( 420 BackendConstants.PARAM_ELAPSED_TIME_ALL, System.currentTimeMillis() - mStartTime); 421 return bundle; 422 } 423 424 /** 425 * Get the job started analytics bundle 426 * 427 * @return analytics bundle 428 */ getJobAttemptedAnalyticsBundle()429 private Bundle getJobAttemptedAnalyticsBundle() { 430 Bundle bundle = new Bundle(); 431 bundle.putString(BackendConstants.PARAM_JOB_ID, mPrintJob.getId().toString()); 432 bundle.putLong(BackendConstants.PARAM_DATE_TIME, System.currentTimeMillis()); 433 // TODO: Add real location 434 bundle.putString(BackendConstants.PARAM_LOCATION, "United States"); 435 bundle.putInt( 436 BackendConstants.PARAM_JOB_PAGES, 437 mPrintJob.getInfo().getCopies() * mPrintJob.getDocument().getInfo().getPageCount()); 438 // TODO: Add real user id 439 bundle.putString(BackendConstants.PARAM_USER_ID, "userid"); 440 // TODO: Determine whether the print job came from share to BIPS or from print system 441 bundle.putString(BackendConstants.PARAM_SOURCE_PATH, "ShareToBips || PrintSystem"); 442 return bundle; 443 } 444 } 445